📜 ⬆️ ⬇️

Template specialization by base class

There are several base classes, heirs and some template handler that performs some actions with an instance of heirs. Its behavior depends on which classes are base for the class being processed. Possible option I want to show.

Suppose we have several base classes and classes that can be inherited from them.
So, we have Base1, Base2, Base3 and classes Derived12, Derived23.

class Derived12: public Base1, public Base2 {}; class Derived23: public Base2, public Base3 {}; 

And there is some class Executor.

 template<typename T> struct Executor { void operator()(const T&); }; 

In this example, we will not use the function argument, just assume that the operator () method will perform some actions with the passed object.
')
The behavior of the operator () method should depend on the base class of the T parameter. It is not possible to simply specialize a template with a base class, since the specialization does not work for the class of the heir. So, you need to add the second argument of the template, which will be some flag for specialization and, of course, you want to automatically check inheritance.

There is a solution, it is described in A. Alexandrescu's book “Modern Design in C ++” in the section “Recognition of Convertibility and Inheritance at the Compilation Stage”. The idea is to use an overload function that accepts different types of parameters and returns different types. To determine the type, Alexandrescu used sizeof (in the edition that I got into my hands), but the decltype operator was added to the C ++ 11 standard. This eliminates the need to write extra code.

So, we rewrite the Executor with the above, and at the same time add at least some implementation for the operator () method:

 template<typename T, typename F> struct Executor { void operator()(const T&) { std::cout << " \n"; } }; template<typename T> struct Executor<T, Base1> { void operator()(const T&) { std::cout << "T   Base1\n"; } }; template<typename T> struct Executor<T, Base3> { void operator()(const T&) { std::cout << "T   Base3\n"; } }; 

The Executor class has been specialized, it remains to do an automatic inheritance check. To do this, we write an overloaded selector function. There is no need to implement it, since it will not be invoked. When receiving the type of the result of calculations by the decltype operator, the calculations themselves are not performed.

 void selector(...); Base1 selector(Base1*); Base3 selector(Base3*); 

When “calling” a function selector with passing a pointer to a class of successor, the compiler will try to choose the best option. If the class is derived from Base1 or Base3, then the appropriate method will be selected; if the class is inherited from something else, then a function with a variable number of arguments will be selected.

Now how to use it:

 void main() { Derived12 d12; Derived23 d23; double d; Executor<Derived12, decltype( selector( (Derived12*) 0 ) )>()( d12 ); Executor<Derived23, decltype( selector( (Derived23*) 0 ) )>()( d23 ); Executor<double, decltype( selector( (double*) 0 ) )>()( d ); } 


The lines will be displayed on the screen:
 T inherited from Base1
 T inherited from Base3
 General option


For convenience and beauty, the call to Executor :: operator () can be wrapped in a template function:

 template<typename T> void execute(const T& v) { Executor<T, decltype( selector( (T*) 0 ) )>()( v ); } void main() { Derived12 d12; Derived23 d23; double d; execute( d12 ); execute( d23 ); execute( d ); } 

It turned out, like, not bad. Now we additionally specialize the behavior when inheriting from Base2. You do not even need to specialize the Executor class, just add an overload to the selector function and try to compile. The compiler will display a message with an error that it cannot choose which version of the selector function to use. How to resolve this situation?

First of all, you need to determine what behavior you want to get when the class is simultaneously inherited from two classes that affect the behavior of the Executor class. Consider some options:

1. One of the classes is more priority and the second is ignored;
2. For a situation, special behavior is necessary;
3. It is necessary to invoke sequentially processing for both classes.

Since 3 points is a special case of 2 points, we will not consider it.

It is necessary that the selector function can recognize variants with double inheritance. To do this, we add the second argument, which will be a pointer to another base class, and consider the task of accepting that with parents1 and Base2, Base1 is of higher priority, and if Base2 and Base3 are present, special behavior is needed. In this case, the overload of the selector function and the execute method will be:

 class Base23 {}; void selector(...); Base1 selector(Base1*, ...); Base1 selector(Base1*, Base2*); Base2 selector(Base2*, ...); Base23 selector(Base2*, Base3*); Base3 selector(Base3*, ...); template<typename T> void execute(const T& v) { Executor<T, decltype( selector( (T*) 0, (T*) 0 ) )>()( v ); } 

The Base23 class does not require implementation, since it will be used only for template specialization. For the Base23 class, the implementation may be empty; without an implementation, there will be a compilation error when determining the overloaded variant of the selector function. The selector function began to take two parameters, if there is simultaneous inheritance from Base1, Base2 and Base3, then you will have to add another argument.

The given method of specializing the behavior of object processing, depending on its base classes, is convenient to use when the number of processed variants is small. For example, if it is necessary to consider only cases when the class is inherited from Base1, Base2 and Base3 at the same time, and for all other cases the behavior will be the same. As for clause 3, when there are several base classes, you need to call up sequential processing for each, it is more convenient to use type lists .

If for some reason it is not possible to use a compiler with support for the C ++ 11 standard, then instead of decltype, you can use sizeof. Additionally, you will need to declare auxiliary classes for the types returned by the selector function. It is important that the sizeof function returns a different value for these classes. In this case, the template Executor class should specialize not with a type, but with an integer value. It will look something like this:

 class IsUnknow { char c; } class IsBase1 { char c[2]; }; class IsBase23 { char c[3]; }; IsUnknow selector(...); IsBase1 selector(Base1*, ...); IsBase1 selector(Base1*, Base2*); IsBase23 selector(Base2*, Base3*); template<typename T> void execute(const T& v) { Executor<T, sizeof( selector( (T*) 0, (T*) 0 ) )>()( v ); } template<typename T, unsigned F> struct Executor { void operator(const T&); } template<typename T> struct Executor<T, sizeof(IsBase1) { void operator(const T&); } template<typename T> struct Executor<T, sizoef(IsBase23) { void operator(const T&); } 


Update : A similar behavior can be implemented with std :: enable_if, which is a bit cumbersome, but the conditions are more explicit. (thanks for the addition of Eivind and lemelisk )
Show implementation ...
 template<typename T> typename std::enable_if< !std::is_base_of<Base2, T>::value && !std::is_base_of<Base1, T>::value && !std::is_base_of<Base3, T>::value, void >::type execute(const T&) { cout << " \n"; } template<typename T> typename std::enable_if< std::is_base_of<Base1, T>::value && !std::is_base_of<Base2, T>::value, void >::type execute(const T&) { cout << "T   Base1\n"; } template<typename T> typename std::enable_if< std::is_base_of<Base1, T>::value && std::is_base_of<Base2, T>::value, void >::type execute(const T&) { cout << "T   Base1  Base2\n"; } template<typename T> typename std::enable_if< std::is_base_of<Base2, T>::value && !std::is_base_of<Base1, T>::value && !std::is_base_of<Base3, T>::value, void >::type execute(const T&) { cout << "T   Base2\n"; } template<typename T> typename std::enable_if< std::is_base_of<Base3, T>::value, void >::type execute(const T&) { cout << "T   Base3\n"; } 



Update2 : You can use links as arguments to the selector function, then calling Executor :: operator () will be a little more understandable.
Show implementation ...
 class Base23 {}; void selector(...); Base1 selector(const Base1&, ...); Base1 selector(const Base1&, const Base2&); Base2 selector(const Base2&, ...); Base23 selector(const Base2&, const Base3&); Base3 selector(const Base3&, ...); template<typename T> void execute(const T& v) { Executor<T, decltype( selector( v, v ) )>()( v ); } 

Source: https://habr.com/ru/post/247081/


All Articles