Pull to refresh

Учим старый-добрый CRTP новым трюкам

Reading time4 min
Views2.3K
Иногда мы делаем вещи, ценность которых является весьма сомнительной. Это как раз тот самый случай.

Код лучше тысячи слов


Не буду тянуть кота за хвост, а перейду сразу к делу. Обычно мы используем 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):
  1. от экземпляра шаблона base_tag компилятор получает тип тэга ,
  2. который, в свою очередь, определён как тип, возвращаемый функцией get_tag(), с указателем Foo* в качестве аргумента,
  3. что вызывает срабатывание механизма перегрузки функций (overload resolution) и даёт функцию, объявленную в классе Base с типом Foo и void в качестве аргументов шаблона, то есть Base<Foo, void>
  4. ???
  5. 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.

MSVC10
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'

GCC4.7
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, скорее всего ему на самом деле совершенно не интересен тип аллокатора (и он получает аллокатор по умолчанию), и вряд ли он имеет ввиду "я хочу std::vector в точности с аллокатором (подразумеваемым) по умолчанию std::allocator". Но делая так, вы ограничиваете область применения вашей сущности (например, функции), которая может взаимодействовать только с вектором с аллокатором по умолчанию. С другой стороны, было бы слишком муторно упоминать аллокатор то тут, то там. Возможно, было бы неплохо иметь для вектора механизм определения аллокатора подобным автоматическим способом, описанным выше.

Выводы


Ну, в этот раз я предлагаю решать вам, стоит ли этот приём чего-то или это всего лишь очередная бесполезная головоломка.
Tags:
Hubs:
+6
Comments0

Articles

Change theme settings