Pull to refresh

Специализация шаблона базовым классом

Reading time 6 min
Views 16K
Есть несколько базовых классов, наследники и некоторый шаблонный обработчик, выполняющий какие-то действия с экземпляром наследников. Его поведение зависит от того, какие классы являются базовыми для обрабатываемого класса. Возможный вариант я хочу показать.

Пусть у нас есть несколько базовых классов и классы, которые могут от них наследоваться.
Итак, имеем Base1, Base2, Base3 и классы Derived12, Derived23.

class Derived12: public Base1, public Base2
{};

class Derived23: 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 унаследован от Base1\n";
    }
};

template<typename T>
struct Executor<T, Base3>
{
    void operator()(const T&)
    {
        std::cout << "T унаследован от Base3\n";
    }
};

Специализация класса Executor выполнена, осталось сделать автоматическую проверку на наследование. Для этого напишем перегруженную функцию selector. Её нет нужды реализовывать, так как она не будет вызываться. При получении типа результата вычислений оператором decltype сами вычисления не выполняются.

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

При «вызове» функции selector c передачей указателя на класс наследник, компилятор постарается выбрать лучший вариант. Если класс является наследником Base1 или Base3, то будет выбран соответсвующий метод, если класс наследуется от чего-то другого, то будет выбрана функция с переменным количеством аргументов.

Теперь о том, как это использовать:

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 );
}


На экран будут выведены строчки:
T унаследован от Base1
T унаследован от Base3
Общий вариант


Для удобства и красоты вызов Executor::operator() можно обернуть в шаблонную функцию:


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 );
}

Получилось, вроде, неплохо. Теперь дополнительно специализируем поведение при наследовании от 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 реализации не требует, так как он будет использоваться только для специализации шаблона. Для класса Base23 реализация может быть пустой, без реализации будет ошибка компиляции при определении перегруженного варианта функции selector. Функция 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&);
}


Update: Аналогичное поведение можно реализовать при помощи std::enable_if, получается немного громоздко, но условия задаются более явно. (спасибо за дополнение Eivind и lemelisk)
Показать реализацию...
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: В качестве аргументов функции 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<typename T>
void execute(const T& v)
{
    Executor<T, decltype( selector( v, v ) )>()( v );
}

Tags:
Hubs:
+10
Comments 19
Comments Comments 19

Articles