Base class specialization
There are several base classes, descendants, and some template handler that performs some actions with the instance of the descendants. Its behavior depends on which classes are base for the class being processed. I want to show a possible option.
Suppose we have several base classes and classes that can be inherited from them.
So, we have Base1, Base2, Base3 and classes Derived12, Derived23.
And there is some class Executor.
In this example, we will not use the function argument, just assume that the operator () method will perform some action on the passed object.
The behavior of the operator () method should depend on the base class of the T. parameter. Just specializing the template in the base class will not work, since specialization will not work for the heir class. So, you need to add the second argument to the template, which will be some flag for specialization and, of course, you want to automatically check the inheritance.
There is a solution, it is described in the book by A. Alexandrescu “Modern Design in C ++” in the section “Recognition of convertibility and inheritance at the compilation stage”. The idea is to use function overloads that accept different types of parameters and return different types. Alexandrescu used sizeof to determine the type (in the edition that fell into my hands), but the decltype operator was added to the C ++ 11 standard. This eliminates the need for writing extra code.
So, we rewrite Executor with the above said and at the same time add at least some implementation for the operator () method:
The specialization of the class Executor is done, it remains to do an automatic check for inheritance. To do this, we write an overloaded selector function. There is no need to realize it, since it will not be called. When obtaining the type of the result of calculations by the decltype operator, the calculations themselves are not performed.
When “calling” the selector function with a pointer to the class, the compiler will try to choose the best option. If the class is an inheritor of Base1 or Base3, then the corresponding 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 about how to use it:
The following lines will be displayed on the screen:
For convenience and beauty, calling Executor :: operator () can be wrapped in a template function:
It turned out, sort of, not bad. Now we additionally specialize the behavior when inheriting from Base2. You don’t even need to specialize the Executor class, just add an overload of the selector function and try to compile. The compiler will give an error message that it cannot choose which option of the selector function to use. How to resolve this situation?
First of all, we need to determine what behavior we want to get when the class is simultaneously inherited from two classes that affect the behavior of the Executor class. Let's consider some options:
1. One of the classes is more priority and the second is ignored;
2. For the situation, special behavior is necessary;
3. You must invoke processing sequentially for both classes.
Since point 3 is a special case of point 2, we will not consider it.
The selector function needs to be able to recognize double inheritance options. To do this, we add the second argument, which will be a pointer to another base class and consider the problem assuming that in the presence of parents Base1 and Base2, Base1 is more priority, and in the presence of Base2 and Base3 special behavior is necessary. In this case, the overload of the selector function and the execute method will look like:
The Base23 class does not require implementation, as it will be used only for template specialization. For the Base23 class, the implementation may be empty, without the implementation there will be a compilation error when defining an overloaded version of the selector function. The selector function began to take two parameters, if there is simultaneous inheritance from Base1, Base2 and Base3, then one more argument will have to be added.
The method of specializing the behavior of processing an object depending on its base classes is conveniently used when the number of processed options is small. For example, if you need to consider only cases when the class inherits from Base1, Base2 and Base3 at the same time, and for all other cases the behavior will be the same. As for item 3, when in the presence of several base classes it is necessary to call 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 you can use sizeof instead of decltype. Additionally, you will need to declare helper classes for the types returned by the selector function. It is important that the sizeof function returns a different value for these classes. The template class Executor in this case should specialize not with a type, but with an integer value. It will look something like this:
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
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 action on the passed object.
The behavior of the operator () method should depend on the base class of the T. parameter. Just specializing the template in the base class will not work, since specialization will not work for the heir class. So, you need to add the second argument to the template, which will be some flag for specialization and, of course, you want to automatically check the inheritance.
There is a solution, it is described in the book by A. Alexandrescu “Modern Design in C ++” in the section “Recognition of convertibility and inheritance at the compilation stage”. The idea is to use function overloads that accept different types of parameters and return different types. Alexandrescu used sizeof to determine the type (in the edition that fell into my hands), but the decltype operator was added to the C ++ 11 standard. This eliminates the need for writing extra code.
So, we rewrite Executor with the above said and at the same time add at least some implementation for the operator () method:
template
struct Executor
{
void operator()(const T&)
{
std::cout << "Общий вариант\n";
}
};
template
struct Executor
{
void operator()(const T&)
{
std::cout << "T унаследован от Base1\n";
}
};
template
struct Executor
{
void operator()(const T&)
{
std::cout << "T унаследован от Base3\n";
}
};
The specialization of the class Executor is done, it remains to do an automatic check for inheritance. To do this, we write an overloaded selector function. There is no need to realize it, since it will not be called. When obtaining 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” the selector function with a pointer to the class, the compiler will try to choose the best option. If the class is an inheritor of Base1 or Base3, then the corresponding 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 about how to use it:
void main()
{
Derived12 d12;
Derived23 d23;
double d;
Executor()( d12 );
Executor()( d23 );
Executor()( d );
}
The following lines will be displayed on the screen:
T inherited from Base1 T inherited from Base3 General option
For convenience and beauty, calling Executor :: operator () can be wrapped in a template function:
template
void execute(const T& v)
{
Executor()( v );
}
void main()
{
Derived12 d12;
Derived23 d23;
double d;
execute( d12 );
execute( d23 );
execute( d );
}
It turned out, sort of, not bad. Now we additionally specialize the behavior when inheriting from Base2. You don’t even need to specialize the Executor class, just add an overload of the selector function and try to compile. The compiler will give an error message that it cannot choose which option of the selector function to use. How to resolve this situation?
First of all, we need to determine what behavior we want to get when the class is simultaneously inherited from two classes that affect the behavior of the Executor class. Let's consider some options:
1. One of the classes is more priority and the second is ignored;
2. For the situation, special behavior is necessary;
3. You must invoke processing sequentially for both classes.
Since point 3 is a special case of point 2, we will not consider it.
The selector function needs to be able to recognize double inheritance options. To do this, we add the second argument, which will be a pointer to another base class and consider the problem assuming that in the presence of parents Base1 and Base2, Base1 is more priority, and in the presence of Base2 and Base3 special behavior is necessary. In this case, the overload of the selector function and the execute method will look like:
class Base23 {};
void selector(...);
Base1 selector(Base1*, ...);
Base1 selector(Base1*, Base2*);
Base2 selector(Base2*, ...);
Base23 selector(Base2*, Base3*);
Base3 selector(Base3*, ...);
template
void execute(const T& v)
{
Executor()( v );
}
The method of specializing the behavior of processing an object depending on its base classes is conveniently used when the number of processed options is small. For example, if you need to consider only cases when the class inherits from Base1, Base2 and Base3 at the same time, and for all other cases the behavior will be the same. As for item 3, when in the presence of several base classes it is necessary to call 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 you can use sizeof instead of decltype. Additionally, you will need to declare helper classes for the types returned by the selector function. It is important that the sizeof function returns a different value for these classes. The template class Executor in this case 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
void execute(const T& v)
{
Executor()( v );
}
template
struct Executor
{
void operator(const T&);
}
template
struct Executor
struct Executor
Update: Аналогичное поведение можно реализовать при помощи std::enable_if, получается немного громоздко, но условия задаются более явно. (спасибо за дополнение Eivind и lemelisk)
Показать реализацию...template
typename std::enable_if<
!std::is_base_of::value &&
!std::is_base_of::value &&
!std::is_base_of::value,
void >::type
execute(const T&)
{
cout << "Общий вариант\n";
}
template
typename std::enable_if<
std::is_base_of::value && !std::is_base_of::value,
void >::type
execute(const T&)
{
cout << "T унаследован от Base1\n";
}
template
typename std::enable_if<
std::is_base_of::value && std::is_base_of::value,
void >::type
execute(const T&)
{
cout << "T унаследован от Base1 и Base2\n";
}
template
typename std::enable_if<
std::is_base_of::value &&
!std::is_base_of::value &&
!std::is_base_of::value,
void >::type
execute(const T&)
{
cout << "T унаследован от Base2\n";
}
template
typename std::enable_if<
std::is_base_of::value,
void >::type
execute(const T&)
{
cout << "T унаследован от Base3\n";
}
Update2: В качестве аргументов функции selector можно использовать ссылки, тогда вызов Executor::operator() будет чуть более понятным. Показать реализацию...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
void execute(const T& v)
{
Executor()( v );
}