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

в 15:36, , рубрики: CRTP, metaprogramming, template, Песочница, метки: , ,

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

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

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

Выводы

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

Автор: toughengineer

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js