Предлагаю на рассмотрение общественности новую схему реализации паттерна 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 класс. Но это уже начало следующей истории
Здравствуйте, remark, Вы писали:
Вам бы это посоветовать в ATL и WTL.
Проблем бы поубавилось
Здравствуйте, remark, Вы писали:
R>Зато тут появляются новые интересные возможности, т.к. теперь "базовому классу" известна *вся* иерархия наследования пользовательских классов, а не только most-derived-type класс. Но это уже начало следующей истории
Не надо нам таких интересных возможностей
Я когда-то на подобной штуке и loki'евских списках типов сделал сериализацию контролов в MFC'шных диалогах, заменив ей попутно DDX. На декларативщину меня потянуло, ага. И все бы ничего, вот только отлаживать это хозяйство можно было лишь с большим трудом — в реальном коде отладчику мигом сносило башню и показать значения переменных он был не в силах
Одним из 33 полных кавалеров ордена "За заслуги перед Отечеством" является Геннадий Хазанов.
Здравствуйте, Roman Odaisky, Вы писали:
RO>Здравствуйте, remark, Вы писали:
R>>Предлагаю на рассмотрение общественности новую схему реализации паттерна CRTP.
RO>А чем это отличается от моего велосипеда: http://rsdn.ru/Forum/message/2515373.1.aspxАвтор: Roman Odaisky
Дата: 05.06.07
?
В целом похоже. Чем-то наверное отличается...
Там у тебя заточено под реализацию всяких сервисов...