• Keine Ergebnisse gefunden

Tag Dispatch With Polymorphic Meta Functions

5.6 • Tag Dispatch With Polymorphic Meta Functions 89

on the argument types, but it happens at compile time and is thus called static polymorphism orcompile time polymorphism.

In the following, we introduce a new type of meta functions that makes this type of polymorphism available to type calculations in addition to run time code.

As shown in Section 2.4.2, C++ meta functions are usually implemented as templated structs, where the arguments to the function are passed as template parameters and the return type can be retrieved as a nested typedef inside the struct. Control flow within the meta function then happens by (partially) specializing the struct.

In order to replicate the polymorphic properties of the iterator example at the beginning of this section, we instead overload a function signature, with the meta function parameters now passed as the types of the actual function arguments and the result of the meta function call encoded in the function’s return type. This is possible because [C++/8.3.5.3] permits overloaded functions to have different return types, and while this feature may be confusing in normal C++ code, it fits perfectly for our application.

Let us consider a very simple example, which lets us register a descriptor based on a single tag type. If we assume that the meta function is called dispatch, we provide an overload that connects the input value X with the return typeY:

1 // no initial declaration of dispatch required for function overloads 2

3 // register X -> Y relation

4 Y dispatch(X*); // only declaration

Note that instead of directly specifying the tag in the argument list, we use a pointer; this allows X to be an incomplete type at the time the meta function is called. As explained earlier, this function only needs to be declared; a possible definition will never be used.

Invoking this meta function is slightly more involved than invoking a struct-based meta function. First of all, we need a way to capture the return type of a function in a typedef. For this purpose, we use the C++11 keyword decltype, which returns the result type of its argument (cf. Section 2.4.3). With its help, we can invoke the meta function through1

1 // utility struct to generate a value of type T*

2 template<typename T>

3 T* declptr();

4

5 // helper struct to encapsulate invocation syntax 6 template<typename Tag>

7 struct invoke_dispatch {

8 typedef decltype(dispatch(declptr<Tag>())) type;

1 For older compiler versions that lack C++11 and decltype, it is possible to use the non-standard extension__typeof__. Apart from some semantic corner cases not relevant in this context, their behavior is identical.

5.6 • Tag Dispatch With Polymorphic Meta Functions 91

9 };

10

11 typedef typename invoke_dispatch<U>::type result;

Note that in order to create the faked call to dispatch() inside decltype, we need to fabricate an argument of type Tag*. While we could do so by writing static_cast<Tag*>(nullptr), the utility functiondeclptr()conveys its meaning much clearer. In the example above, we have wrapped the real dispatch function in a traditional meta function called invoke_dispatchwhich provides an interface more familiar to users and encapsulates the slightly involved call to dispatch. In this example, the overall result looks very similar to a “traditional” meta function, but the two approaches actually use different lookup mechanisms for resolving the general meta function to the correct version for its arguments: Traditional meta functions rely on template specialization, while our function-based meta functions are based on function overload resolution. These two mechanisms match their arguments according to different rule sets:

• An instantiated template is looked up by first searching for a complete specialization [C++/14.7.3], progressively widening the search scope to partial template specializations [C++/14.5.5.2] and finally falling back to the primary template definition.

• Function overload resolution as defined by [C++/13.3] is considerably more complex, in particular because [C++/13.3.2] permits implicit conversions of the function arguments to find a viable function. As there are multiple ways for the compiler to perform an implicit conversion, there can be a large number of candidate functions obtained by applying different conversions to the arguments. Consequently, the rule set in [C++/13.3.3] that prioritizes those candidate functions is considerably more complex than the class template instantiation rules, especially in the presence of function templates or even function template specializations.

Importantly, the template instantiation mechanism always matches very specific argument types (or, in the case of partial specializations, possibly specific templates with arbitrary inner template arguments). Due to the very nature of templates in C++, the logic responsible for finding a matching specialization does not take into account any relationships between types, such as inheritance or conversion operators This restriction requires the explicit registration of every single supported tag by specializing the dispatch meta function. Function overload resolution on the other hand knows about type relations and will in particular cast a type to one of its base classes if there is a function overload that takes the base class as a parameter, but no overload for the type itself.

In the context of TypeTree, this property of the function overload resolution process makes it possible to create a hierarchy of ImplementationTags similar to the iterator example at the beginning of this section. While the base tag will

have to be registered for all transformations, derived tags only need to provide more specialized transformation descriptors if their behavior should differ from the base tag for this specific transformation. This functionality is used by PDE-Lab to implement a VectorGridFunctionSpace, which is a special case of the PowerGridFunctionSpace for variables that represent spatial vectors (e.g. a veloc-ity). The implementation tag of this VectorGridFunctionSpace inherits from the tag of the PowerGridFunctionSpace and is thus able to reuse the majority of its implementation. It only overloads a very small number of transformations to e.g.

output its values as VTKvector data, while the normal PowerGridFunctionSpace outputs its children as separate scalar data sets.

Another advantage of function overloading is the additional flexibility in where to place the registration declarations:

• Class template specializations have to be placed in the same scope as the primary template [C++/14.5.5], which is inconvenient in the context of meta functions because the namespace of the specialized meta function will often be different from the application namespace with the user types.

• On the other hand, function overloads will be found in a much larger number of scopes as long as the function call is not qualified with a specific scope.

In addition to the scope of the call site, ADL will also consider the scopes of all arguments (both regular and template ones) when looking for candidate functions, which enables users to place the function declaration registering the dispatch pattern directly into the application namespace(s).