Иногда мы делаем вещи, ценность которых является весьма сомнительной. Это как раз тот самый случай.
Код лучше тысячи слов
Не буду тянуть кота за хвост, а перейду сразу к делу. Обычно мы используем CRTP примерно так:
template<typename type, typename tag>
struct Base
{};
template<typename type, typename tag>
void f(const Base<type, tag>&)
{}
struct Foo : Base<Foo, void>
{};
int main()
{
Foo foo;
f(foo);
}
Функции f()
на самом деле всё равно, какой тэг у её аргумента, и она принимает объект любого типа, унаследованого от Base
. Но не будет ли более удобным, если мы просто опустим не интересующий нас тэг? Зацените:
template<typename t>
struct base_tag { typedef decltype(get_tag((t*)42)) type; };
template<typename type,
typename tag = typename base_tag<type>::type>
struct Base
{
friend tag get_tag(type*); //never defined
};
template<typename type>
void f(const Base<type>&)
{}
struct Foo : Base<Foo, void>
{};
int main()
{
Foo foo;
f(foo);
}
Теперь, посмотрев на объявление f()
, мы интуитивно поймём, что функции действительно всё равно, какой тэг у её аргумента.
Как это работает
В классе Base
объявляется функция-друг, возвращающая тэг и принимающая указатель на унаследованный тип. Заметьте, что эта функция не нуждается в определении. Когда мы определяем тип, например, Foo
, мы фактически объявляем функцию со соответствующим прототипом, в данном случае:
void get_tag(Foo*);
Когда мы вызываем f()
, при этом создавая экземпляр шаблона (template instantiation), компилятор пытается определить аргумент шаблона по умолчанию для аргумента функции (который является объектом класса Foo
):
- от экземпляра шаблона
base_tag
компилятор получает тип тэга , - который, в свою очередь, определён как тип, возвращаемый функцией
get_tag()
, с указателемFoo*
в качестве аргумента, - что вызывает срабатывание механизма перегрузки функций (overload resolution) и даёт функцию, объявленную в классе
Base
с типомFoo
иvoid
в качестве аргументов шаблона, то естьBase<Foo, void>
- ???
- Profit!
То есть, круг замкнулся!
ECRTP
Ничего лучше не придумав, я называю это «excessively-curious’ly recurring template pattern». Так что ещё оно может?
Если мы действительно хотим этого, тэг может быть указан явно:
template<typename type>
void g(const Base<type, int>&)
{}
struct Bar : Base<Bar, int>
{};
int main()
{
Foo foo;
Bar bar;
f(foo);
f(bar);
g(foo); //doesn't compile by design
g(bar);
}
Заметьте, что g(foo)
намеренно не позволит скомпилировать код, потому что тэг аргумента должен быть типом int
(в то время как он является типом void
для Foo
). В такой ситуации компилятор выдаёт красивое сообщение об ошибке. Ну, по крайней мере MSVC10 и GCC4.7.
main.cpp(30): error C2784: 'void g(const Base<type,int> &)' : could not deduce template argument for 'const Base<type,int> &' from 'Foo' main.cpp(18) : see declaration of 'g'
source.cpp: In function 'int main()': source.cpp:30:8: error: no matching function for call to 'g(Foo&)' source.cpp:30:8: note: candidate is: source.cpp:18:6: note: template<class type> void g(const Base<type, int>&) source.cpp:18:6: note: template argument deduction/substitution failed: source.cpp:30:8: note: mismatched types 'int' and 'void' source.cpp:30:8: note: 'Foo' is not derived from 'const Base<type, int>'
Даже лучше, чем у MSVC!
Также можно задать тэг по умолчанию:
template<typename type>
void get_tag(type*); //default tag is 'void'
template<typename t>
struct base_tag { typedef decltype(get_tag((t*)42)) type; };
template<typename type,
typename tag = typename base_tag<type>::type>
struct Base
{
friend tag get_tag(type*); //never defined
};
struct Foo : Base<Foo> //tag defaults to void
{};
Определение выше эквивалентно
struct Foo : Base<Foo, void>
{};
Так что теперь мы можем считать, что нет вообще никакого тэга и оставить этот функционал для продвинутого использования.
Что насчёт C++98?
Более старые компиляторы не поддерживают ключевое слово decltype
. Но если у вас конечное число тэгов (или чего бы то ни было), вы можете использовать приём с sizeof
(sizeof
trick):
struct tag1 {}; //a set of tags
struct tag2 {};
struct tag3 {};
#define REGISTER_TAG(tag, id) char (&get_tag_id(tag))[id];
template<> struct tag_by_id<id>
{ typedef tag type; };
template<unsigned> struct tag_by_id;
REGISTER_TAG(tag1, 1) //defines id's
REGISTER_TAG(tag2, 2)
REGISTER_TAG(tag3, 42)
template<typename t>
struct base_tag
{
enum
{
tag_id = sizeof(get_tag_id(get_tag((t*)42)))
};
typedef typename tag_by_id<tag_id>::type type;
};
template<typename type,
typename tag = typename base_tag<type>::type>
struct Base
{
friend tag get_tag(type*); //never defined
};
template<typename type>
void f(const Base<type>&)
{}
struct Foo : Base<Foo, tag1>
{};
int main()
{
Foo foo;
f(foo);
}
Немного многословно, но зато работает.
Лишние телодвижения?
Так действительно ли всё это лишние телодвижения?
Мы уже видели, что этот приём делает код чуточку красивее. Давайте посмотрим, что будет в случае двух аргументов. Конечно же, мы можем написать код так:
template<class type1, class tag1, class type2, class tag2>
void h(const Base<type1, tag1>&, const Base<type2, tag2>&)
{}
Даже более короткое ключевое слово class
не делает код существенно короче.
Сравните с этим:
template<class type1, class type2>
void h(const Base<type1>&, const Base<type2>&)
{}
Тэг? Не, не слышал…
Фантастическую ситуацию с тремя и более аргументами вы можете представить сами.
Мысль такова: если нас не интересует некая вещь, должна ли она обязательно быть явной? Когда кто-то пишет std::vector<int>
, скорее всего ему на самом деле совершенно не интересен тип аллокатора (и он получает аллокатор по умолчанию), и вряд ли он имеет ввиду «я хочу std::vector
в точности с аллокатором (подразумеваемым) по умолчанию std::allocator
». Но делая так, вы ограничиваете область применения вашей сущности (например, функции), которая может взаимодействовать только с вектором с аллокатором по умолчанию. С другой стороны, было бы слишком муторно упоминать аллокатор то тут, то там. Возможно, было бы неплохо иметь для вектора механизм определения аллокатора подобным автоматическим способом, описанным выше.
Выводы
Ну, в этот раз я предлагаю решать вам, стоит ли этот приём чего-то или это всего лишь очередная бесполезная головоломка.
Автор: toughengineer