[Trick] CRTP v2.0
От: remark Россия http://www.1024cores.net/
Дата: 28.09.07 14:57
Оценка: 110 (11) +1
Предлагаю на рассмотрение общественности новую схему реализации паттерна CRTP.

Для начала классическая схема реализации CRTP:

template<typename derived_t>
struct CRTP 
{
};

struct A : CRTP<A>
{
};


Паттерн CRTP сам по себе очень мощный — вот пример того, что он может:

struct I
{
    virtual I* clone() const = 0;
};

template<typename derived_t>
struct CRTP : I
{
    virtual I* clone() const
    {
        // автоматически реализуем функцию клонирования
        return new derived_t(*static_cast<derived_t const*>(this));
    }

    void f()
    {
        // создаём временный объект "реального" типа
        derived_t temp; (void) temp;
        // используем sizeof "реального" типа
        size_t s = sizeof(derived_t);
        // вызываем функцию "реального" производного типа без виртуальных функций
        static_cast<derived_t*>(this)->derived_t::g();
    }
};

struct A : CRTP<A>
{
    void g() {}
};


Пока мы наследуем только от CRTP — всё замечательно. Проблемы начинаются, когда мы хотим отнаследовать ещё один класс от А:

template<typename derived_t>
struct CRTP
{
    void f()
    {
        derived_t temp; (void) temp;
        size_t s = sizeof(derived_t);
        static_cast<derived_t*>(this)->derived_t::g();
    }
};

struct A : CRTP<A>
{
    void g() {}
};

struct B : A
{
    void g() {}
    // Оп-ля. В CRTP::f() будет создавать временный объект А, а не В.
    // sizeof будет браться тоже от А; и g() будет вызываться из А, а не из В
};


Решение в лоб заключается в добавлении промежуточного шаблонного класса-посредника:

template<typename derived_t>
struct CRTP
{
    void f()
    {
        derived_t temp; (void) temp;
        size_t s = sizeof(derived_t);
        static_cast<derived_t*>(this)->derived_t::g();
    }
};

template<typename derived_t>
struct A_ : CRTP<derived_t>
{
    void g() {}
};

struct A : A_<A>
{
    // пустой, просто что бы сделать шаблон A_ реальным типом
};

struct B : A_<B>
{
    void g() {}
};


Старые проблемы решили, но приобрели новые. Во-первых, любой класс, от которого можно наследоваться (а это никогда наперёд не известно, т.е. фактически каждый класс) должен стать шаблонным (в примере A_). А это подразумевает перенос всей реализации со всеми необходимыми для реализации include'ами в h-файл. И добавление к определениям всех функций template<typename derived_t>. Можно представить во что это выливается для класса диалога с несколькими десятками функций и кучей зависимостей.
Во-вторых, допустим в базовом классе был enum:
template<typename derived_t>
struct A_ : CRTP<derived_t>
{
    enum E {E1, E2, E3};
    void g() {}
};

Теперь в классах А и В это получаются уже 2 разных enum'а! Плюс уже нельзя писать просто А_::E1, т.к. А_ — не тип, а шаблон. Как это рашить непонятно. Если перенести enum в реальный класс А, то его не будет видно в классе В...

Фактически эти проблемы сводят на нет возможность использования CRTP во многих случаях, особенно в базовых библиотечных классах, от которых должны наследоваться пользовательские.

Ниже приводится схема, которая не имеет указанных недостатков. Т.е. любой класс, использующий CRTP может стать базовым без каких-либо модификаций.

struct CRTP_base
{
};

template<typename derived_t, typename base_t = CRTP_base>
struct CRTP : base_t
{
    void f()
    {
        derived_t temp; (void) temp;
        size_t s = sizeof(derived_t);
        static_cast<derived_t*>(this)->derived_t::g();
    }
};

struct A : CRTP<A>
{
    enum E {E1, E2, E3};
    void g() {}
};

struct B : CRTP<B, A>
{
    void g() {}
};

struct C : CRTP<C, B>
{
    void g() {}
};

struct C2 : CRTP<C2, B>
{
    void g() {}
};

void test()
{
    A().f();
    B().f();
    C().f();
    C2().f();
}


Справедливость восстановлена! Классы не шаблонные. От любого класса можно прозрачно наследовать. В А находится enum, который один и к которому можно нормально обращаться.

Получается следующая иерархия наследования:
C -> CRTP<C, B> -> B -> CRTP<B, A> -> A -> CRTP<A, CRTP_base> -> CRTP_base
При этом она линейная, т.е. sizeof не распухает и все касты работают нормально.

Я пока отметил тут только один новый недостаток — в шаблон CRTP надо добавить "форвардинг" конструкторов в базовый класс. Слава богу, это можно решить только в классе CRTP, без привлечения производных. Ну и конечно продолжаем молиться на C++0x

Зато тут появляются новые интересные возможности, т.к. теперь "базовому классу" известна *вся* иерархия наследования пользовательских классов, а не только most-derived-type класс. Но это уже начало следующей истории



1024cores &mdash; all about multithreading, multicore, concurrency, parallelism, lock-free algorithms
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.