Learn good old CRTP new tricks
Sometimes we do things whose value is highly doubtful. This is exactly the case.
I will not pull the cat by the tail, but I will go straight to the point. Usually we use CRTP like this:
A function
Now, looking at the declaration
A
When we call
That is, the circle is closed!
Without inventing anything better, I call it “excessively-curious'ly recurring template pattern”. So what else can it?
If we really want this, the tag can be specified explicitly:
Note that
You can also set a default tag:
The definition above is equivalent
So now we can assume that there is no tag at all and leave this functionality for advanced use.
Older compilers do not support the keyword
A little verbose, but it works.
So are all these extra gestures really?
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:
Even a shorter keyword
Compare with this:
Tag? No, I haven’t heard ...
You can imagine a fantastic situation with three or more arguments yourself.
The idea is this: if we are not interested in a certain thing, should it necessarily be explicit? When someone writes
Code is better than a thousand words
I will not pull the cat by the tail, but I will go straight to the point. Usually we use CRTP like this:
template
struct Base
{};
template
void f(const Base&)
{}
struct Foo : Base
{};
int main()
{
Foo foo;
f(foo);
}
A function
f()doesn't really care what tag its argument has, and it takes an object of any type inherited from Base. But wouldn’t it be more convenient if we just omit the tag that doesn’t interest us? Check it out:template
struct base_tag { typedef decltype(get_tag((t*)42)) type; };
template::type>
struct Base
{
friend tag get_tag(type*); //never defined
};
template
void f(const Base&)
{}
struct Foo : Base
{};
int main()
{
Foo foo;
f(foo);
}
Now, looking at the declaration
f(), we will intuitively understand that functions really don't care what tag its argument has.How it works
A
Basefriend function is declared in the class that returns a tag and takes a pointer to an inherited type. Note that this function does not need to be defined. When we define a type, for example, Foowe actually declare a function with the corresponding prototype, in this case:void get_tag(Foo*);
When we call
f(), while creating an instance of the template (template instantiation), the compiler tries to determine the default template argument for the function argument (which is an object of the class Foo):- the
base_tagcompiler gets the tag type from the template instance , - which, in turn, is defined as the type returned by the function
get_tag(), with a pointerFoo*as an argument, - which triggers the overload resolution mechanism and gives a function declared in the class
Basewith typeFooandvoidas template arguments, i.e.Base - ???
- Profit!
That is, the circle is closed!
ECRTP
Without inventing anything better, I call it “excessively-curious'ly recurring template pattern”. So what else can it?
If we really want this, the tag can be specified explicitly:
template
void g(const Base&)
{}
struct Bar : Base
{};
int main()
{
Foo foo;
Bar bar;
f(foo);
f(bar);
g(foo); //doesn't compile by design
g(bar);
}
Note that
g(foo)intentionally will not allow compilation of the code, because the argument tag must be a type int(while it is a type voidfor Foo). In this situation, the compiler generates a beautiful error message. Well, at least MSVC10 and GCC4.7.MSVC10
main.cpp (30): error C2784: 'void g (const Base&) ': could not deduce template argument for 'const Base & '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: templateEven better than MSVC!void g (const Base &) 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 ''
You can also set a default tag:
template
void get_tag(type*); //default tag is 'void'
template
struct base_tag { typedef decltype(get_tag((t*)42)) type; };
template::type>
struct Base
{
friend tag get_tag(type*); //never defined
};
struct Foo : Base //tag defaults to void
{};
The definition above is equivalent
struct Foo : Base
{};
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 keyword
decltype. But if you have a finite number of tags (or anything else), you can use the technique to sizeof( sizeoftrick the):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\
{ typedef tag type; };
template struct tag_by_id;
REGISTER_TAG(tag1, 1) //defines id's
REGISTER_TAG(tag2, 2)
REGISTER_TAG(tag3, 42)
template
struct base_tag
{
enum
{
tag_id = sizeof(get_tag_id(get_tag((t*)42)))
};
typedef typename tag_by_id::type type;
};
template::type>
struct Base
{
friend tag get_tag(type*); //never defined
};
template
void f(const Base&)
{}
struct Foo : Base
{};
int main()
{
Foo foo;
f(foo);
}
A little verbose, but it works.
Extra body movements?
So are all these extra gestures really?
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
void h(const Base&, const Base&)
{}
Even a shorter keyword
classdoes not make the code significantly shorter. Compare with this:
template
void h(const Base&, const Base&)
{}
Tag? No, I haven’t heard ...
You can imagine a fantastic situation with three or more arguments yourself.
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". Но делая так, вы ограничиваете область применения вашей сущности (например, функции), которая может взаимодействовать только с вектором с аллокатором по умолчанию. С другой стороны, было бы слишком муторно упоминать аллокатор то тут, то там. Возможно, было бы неплохо иметь для вектора механизм определения аллокатора подобным автоматическим способом, описанным выше.
Выводы
Ну, в этот раз я предлагаю решать вам, стоит ли этот приём чего-то или это всего лишь очередная бесполезная головоломка.