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

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

Пусть у нас есть несколько базовых классов и классы, которые могут от них наследоваться.
Итак, имеем 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 );
}

Ads
AdBlock has stolen the banner, but banners are not teeth — they will be back

More

Comments 19

    +1
    Мне кажется, если требуется такое, обычно что-то не так с архитектурой
      +1
      На самом деле не всегда — иногда нужно дать более простой и наглядный способ выполнять сложные вещи. Если у вас достаточно времени и знаний то такие трюки снабжённые коротким комментарием гораздо лучше разлапистой документации когда и какую вещь следует использовать.
        +1
        Хочется понять реальный пример, когда такое необходимо. В постановке задачи сказано, что «поведение зависит от того, какие классы являются базовыми для обрабатываемого класса». А когда такое вообще нужно? Какова исходная постановка задачи?

        Создается ощущение, что в постановку задачи проник способ решения, что выглядит странным.
          0
          Есть примерно такая ситуация:

          struct Interface
          {
              void getIds(unsigned& id0, unsigned& id1) = 0;
          };
          
          struct HasMainId0
          {
              unsigned id0;
          };
          
          struct HasMainId1: public HasMainId0
          {
              unsigned id1;
          };
          
          struct HasAnotherId
          {
              unsigned anotherId;
          }
          
          struct Data1: HasMainId0 {};
          
          struct Data2: HasMainId1 {};
          
          struct Data3: HasAnotherId {};
          
          struct Data4: HasMainId0, HasAnotherId {};
          
          template<typename T>
          struct DynamicData: public Interface, public T
          {
              void getIds(unsigned& id0, unsigned& id1);
          }
          
          typedef DynamicData<Data1> DynamicData1;
          typedef DynamicData<Data2> DynamicData2;
          typedef DynamicData<Data3> DynamicData3;
          typedef DynamicData<Data4> DynamicData4;
          


          Метод getIds должен возвращать значения:
          HasMinaId0::id0 и 0xFFFFFFFF если класс T унаследован от HasMainId0
          HasMinaId0::id0 и 0xFFFFFFFF если класс T унаследован от HasMainId0 и HasAnotherId
          HasAnotherId::anotherId и 0xFFFFFFFF если класс T унаследован от HasAnotherId
          HasMainId0::id0 и HasMainId1::id1 если класс T унаследован от HasMainId1

          структур Has… больше чем три :) но в этом месте обрабатываются только эти. Для реализации метода getIds и был написан Executor подобный описанному:

          template<typename T, typename F>
          struct GetterIds
          {
              void operator()( const T& val, unsigned& id0, unsigned& id1 );
          };
          
          void selector(... );
          HasMainId0 selector(HasMainId0*, ... );
          HasMainId0 selector(HasMainId0*, HasAnotherId* );
          HasAnotherId selector(HasAnotherId*, ... );
          HasMainId1 selector(HasMainId1*, ... );
          
          


          Общая архитектура досталась в наследство, только базовых структур не было, все поля содержались в структурах Data1, Data2, ..., DataN. И использовалось несколько шаблонных классов в зависимости от того какие параметры содержала та или иная «Data» и в них по разному был реализован метод getIds.

          В этом конкретном месте такой вариант проверки наследования мне показался удобным и более коротким по записи. Там где идет обработка по всем «базовым» структурам я использую списки типов.

            0
            Я может не вник до конца, но мне кажется всю логику Executor нужно просто засунуть в базовые классы, например.

            Общая архитектура досталась в наследство
            Т.е. предложенный метод в какой-то степени является замысловатым костылем.
              0
              Придется писать в структурах Data1...DataN практически один и тот же код для метода getIds. В предложенном мной варианте, есть несколько возможных имплементаций которые собраны в одном месте. И не надо задумывать, при добавлении новой DataN+1, какой должна быть реализация метода getIds.

              Т.е. предложенный метод в какой-то степени является замысловатым костылем.


              Это не костыль, это замена ручного указания на то как должен быть реализован метод. DynamicData формируется при помощи макросов, для каждого варианта DataM в макрос передавалось количество id (от 0 до 2), которое содержится в DataM. И для каждого количества была отдельная реализация шаблонного класса.

              А общая архитектура хоть и имеет некоторые недостатки, но в целом доказала свою жизнеспособность. Рефакторинг производился не самой архитектуры в целом, а механизма извлечения дополнительной информации иэ структур DataM. И заодно немного автоматизировал получение базовой информации.
                0
                А если так:
                struct HasAnotherId
                {
                  virtual void getIds(unsigned& id0, unsigned& id1)
                  {
                    id0 = HasAnotherId::anotherId; id1=0xFFFFFFFF;
                  }
                }
                
                struct HasMainId0 : public HasAnotherId
                {
                  virtual void getIds(unsigned& id0, unsigned& id1)
                  {
                    id0 = HasMainId0::id0; id1=0xFFFFFFFF;
                  }
                }
                
                struct HasMainId1: public HasMainId0
                {
                  virtual void getIds(unsigned& id0, unsigned& id1)
                  {
                    id0 = HasMainId0::id0; id1=HasMinaId1::id1;
                  }
                }
                
                  0
                  Прошу прощения, забыл указать вариант когда нет наследования ни от HasMainId0, HasMainId1, HasAnotherId, тогда возвращается для обоих 0xFFFFFFFF.

                  Да, такой вариант тоже возможен. Единственное, что так как есть еще общий интерфейс в шаблоне для DynamicData нужно будет его реализовать и вызывать из него уже getIds реализованый в DataM. А для случая который я забыл указать можно добавить наследование от структуры.

                  struct NoIds
                  {
                    virtual void getIds(unsigned& id0, unsigned& id1)
                    {
                      id0 = 0xFFFFFFFF; id1=0xFFFFFFFF;
                    }
                  }
                  


                  Но такой вариант мне кажется менее красивым.
                    +1
                    Красота конечно понятие растяжимое, но вот это вот:
                    Executor<T, sizeof( selector( (T*) 0, (T*) 0 ) )>()( v );
                    По мне так больше похоже на regexp чем на C++. Представьте кто-то кроме вас попробует разобраться? Вы тогда в комменты к коду эту статью добавьте :)
                      0
                      Пока я писал код и статью, мне это казалось прозрачным :). Но ваш вопрос заставил сомневаться и пересмотреть код еще раз. Пожалуй в указанном месте можно вообще было обойтись без указателей:

                      selector 
                      Executor<T, sizeof( selector( v, v ) )>()( v );
                      


                      Но Ваш вариант, действительно, гораздо проще. Увлекся я что-то с шаблонами не к месту :)
                        0
                        Ваш вариант с академической точки зрения интереснее. А также, если код базовых класов недоступен для редактирования.
                          0
                          Спасибо.
      +8
      template<class T>
      typename std::enable_if<std::is_base_of<Base1,T>::value,void>::type
      execute(const T&);
      
      template<class T>
      typename std::enable_if<std::is_base_of<Base2,T>::value,void>::type
      execute(const T&);
      
        +1
        Два void'а необязательны
          +1
          В случае проверки на наследование от одного класса указанные Вами пример проще, но как только появляется необходимость проверки на одновременное наследование от нескольких классов такой вариант становиться более громоздкий, чем вариант описанные мной в публикации.

          И опять же надо решать проблему когда класс T одновременно унаследован от Base1 и Base2.
            +1
            Можно же любое условие задать, например, «унаследован от Base1, но не от Base2»:
            typename std::enable_if<
                    std::is_base_of<Base1,T>::value && !std::is_base_of<Base2,T>::value
                >::type
            
              0
              Спасибо, так действительно проще для восприятия и такой вариант более гибкий. Похоже мой вариант больше подходит когда стандарт С++11 не доступен и не используется boost.
          0
          Наверное, не Deliver, а Derived?
            0
            Вы правы, спасибо, исправил.

          Only users with full accounts can post comments. Log in, please.