Есть несколько базовых классов, наследники и некоторый шаблонный обработчик, выполняющий какие-то действия с экземпляром наследников. Его поведение зависит от того, какие классы являются базовыми для обрабатываемого класса. Возможный вариант я хочу показать.
Пусть у нас есть несколько базовых классов и классы, которые могут от них наследоваться.
Итак, имеем Base1, Base2, Base3 и классы Deliver12, Deliver23.
class Deliver12: public Base1, public Base2
{};
class Deliver23: public Base2, public Base3
{};
И есть некоторый класс Executor.
template<typename T>
struct Executor
{
void operator()(const T&);
};
В этом примере использовать аргумент функции не будем, просто предположим, что метод operator() будет выполнять какие-то действия с переданным объектом.
Поведение метода operator() должно зависеть от базового класса параметра T. Просто специализировать шаблон базовым классом не получится, так как специализация не сработает для класса наследника. Значит, необходимо добавить второй аргумент шаблона, который будет некоторым флагом для специализации и, конечно же, хочется автоматически проверять наследование.
Решение есть, оно описано в книге А. Александреску «Современное проектирование на C++» в разделе «Распознавание конвертируемости и наследования на этапе компиляции». Идея состоит в использовании перегрузки функции принимающей разные типы параметров и возвращающей разные типы. Для определения типа Александреску использовал sizeof (в той редакции, что попалась мне в руки), но в стандарт C++11 был добавлен оператор decltype. Это избавляет от написания лишнего кода.
Итак, перепишем Executor с учетом выше сказанного и заодно добавим хоть какую-нибудь реализацию для метода operator():
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 унаследован от Base1n";
}
};
template<typename T>
struct Executor<T, Base3>
{
void operator()(const T&)
{
std::cout << "T унаследован от Base3n";
}
};
Специализация класса Executor выполнена, осталось сделать автоматическую проверку на наследование. Для этого напишем перегруженную функцию selector. Её нет нужды реализовывать, так как она не будет вызываться. При получении типа результата вычислений оператором decltype сами вычисления не выполняются.
void selector(...);
Base1 selector(Base1*);
Base3 selector(Base3*);
При «вызове» функции selector c передачей указателя на класс наследник, компилятор постарается выбрать лучший вариант. Если класс является наследником Base1 или Base3, то будет выбран соответсвующий метод, если класс наследуется от чего-то другого, то будет выбрана функция с переменным количеством аргументов.
Теперь о том, как это использовать:
void main()
{
Deliver12 d12;
Deliver23 d23;
double d;
Executor<Deliver12, decltype( selector( (Deliver12*) 0 ) )>()( d12 );
Executor<Deliver23, decltype( selector( (Deliver23*) 0 ) )>()( d23 );
Executor<double, decltype( selector( (double*) 0 ) )>()( d );
}
На экран будут выведены строчки:
T унаследован от Base1 T унаследован от Base3 Общий вариант
Для удобства и красоты вызов Executor::operator() можно обернуть в шаблонную функцию:
template<typename T>
void execute(const T& v)
{
Executor<T, decltype( selector( (T*) 0 ) )>()( v );
}
void main()
{
Deliver12 d12;
Deliver23 d23;
double d;
execute( d12 );
execute( d23 );
execute( d );
}
Получилось, вроде, неплохо. Теперь дополнительно специализируем поведение при наследовании от Base2. Не нужно даже специализировать класс Executor, достаточно добавить перезгрузку функции selector и попробовать скомпилировать. Компилятор выдаст сообщение с ошибкой, что он не может выбрать какой вариант функции selector использовать. Как разрешить такую ситуацию?
В первую очередь нужно определить какое поведение хотим получить когда класс одновременно унаследован от двух классов, которые влияют на поведение класса Executor. Рассмотрим некоторые варианты:
1. Один из классов более приоритен и второй игнорируем;
2. Для ситуации необзодимо специальное поведение;
3. Необходимо вызвать последовательно обработку для обоих классов.
Так как 3 пункт является частным случаем 2 пункта, то его рассматривать не будем.
Нужно чтобы функция selector могла распознать варианты с двойным наследованием. Для этого добавим второй аргумент, который будет указателем на другой базовый класс и рассмотрим задачу приняв, что при наличии родителей Base1 и Base2 более приоритетным является Base1, а при наличии Base2 и Base3 необходимо специальное поведение. В таком случае перегрузка функции selector и методо execute будут иметь вид:
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 );
}
Класс Base23 реализации не требует, так как он будет использоваться только для специализации шаблона. Функция selector стала принимать два параметра, если будет одновременное наследование от Base1, Base2 и Base3, то придется добавлять еще один аргумент.
Приведенный метод специализации поведения обработки объекта в зависимости от его базовых классов удобно использовать тогда, когда количество обрабатываемых вариантов мало. Например, если необходимо рассмотреть только случаи, когда класс наследуется от Base1, Base2 и Base3 одновременно, а для всех остальных случаях поведение будет одинаковым. Что касается пункта 3, когда при наличии нескольких базовых классов нужно вызвать последовательную обработку для каждого, то удобнее использовать списки типов.
Если по каким-то причинам нет возможности использовать компилятор с поддержкой стандарта C++11, то вместо decltype можно воспользоваться sizeof. Дополнительно нужно будет объявить вспомогательные классы для типов возвращаемых функцией selector. Важно, чтобы функция sizeof возвращала для этих классов разное значение. Шаблоный класс Executor в таком случае должен специализироваться не типом, а целочисленным значением. Выглядеть это будет примерно так:
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&);
}
Автор: Hokum