📜 ⬆️ ⬇️

Learning good old CRTP new tricks

Sometimes we do things whose value is highly dubious. This is exactly the case.

A code is better than a thousand words


I will not pull the cat by the tail, but go straight to the point. We usually use CRTP like this:
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); } 

The functions f() really don't care what tag is in its argument, and it accepts an object of any type inherited from Base . But wouldn't it be more convenient if we just omit the tag that is not interesting to us? Check out:
 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); } 

Now, looking at the declaration of f() , we intuitively understand that functions really do not care what the tag of its argument.

How it works


In the Base class, a friend function is declared, which returns a tag and accepts a pointer to an inherited type. Note that this function does not need to be defined. When we define a type, for example, Foo , we actually declare the function with the corresponding prototype, in this case:
 void get_tag(Foo*); 

')
When we call f() , while creating a template instance (template instantiation), the compiler tries to determine the default template argument for the function argument (which is an object of the Foo class):
  1. from the base_tag template base_tag compiler gets the tag type,
  2. which, in turn, is defined as the type returned by the get_tag() function, with pointer Foo* as an argument,
  3. which triggers the overload resolution mechanism and gives the function declared in the Base class with the type Foo and void as template arguments, that is, Base<Foo, void>
  4. ???
  5. Profit!

That is, the circle is closed!

ECRTP


Not inventing anything better, I call it the “excessively-curious'ly recurring template pattern”. So what else can it do?

If we really want this, the tag can be specified explicitly:
 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); } 


Note that g(foo) intentionally does not allow compiling the code, because the argument tag must be of type int (while it is void for Foo ). In such a situation, the compiler gives a beautiful error message. Well, at least MSVC10 and 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>'
Even better than MSVC!


You can also set a default tag:
 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 {}; 


The definition above is equivalent to
 struct Foo : Base<Foo, void> {}; 


So now we can assume that there is no tag at all and leave this functionality for advanced use.

What about C ++ 98?


Older compilers do not support the decltype keyword. But if you have a finite number of tags (or whatever), you can use the trick with 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); } 

A little wordy, but it works.

Extra body movements?


So is this really all the extra movement?

We have already seen that this technique makes the code a little more beautiful. Let's see what happens in the case of two arguments. Of course, we can write code like this:
 template<class type1, class tag1, class type2, class tag2> void h(const Base<type1, tag1>&, const Base<type2, tag2>&) {} 

Even a shorter class keyword does not make the code much shorter.

Compare with this:
 template<class type1, class type2> void h(const Base<type1>&, const Base<type2>&) {} 

Tag? No, have not heard…

You can imagine a fantastic situation with three or more arguments.

The idea is this: if we are not interested in a certain thing, should it necessarily be explicit? When someone writes std::vector, ( ), " std::vector () std::allocator ". , (, ), . , , . , , .


, , - .
std::vector, ( ), " std::vector () std::allocator ". , (, ), . , , . , , .


, , - .

std::vector, ( ), " std::vector () std::allocator ". , (, ), . , , . , , .


, , - .

std::vector, ( ), " std::vector () std::allocator ". , (, ), . , , . , , .


, , - .

Source: https://habr.com/ru/post/152387/


All Articles