Consider the following two pieces of code with a CRTP pattern:
template <typename Derived>
struct Base1 {
int baz(typename Derived::value_type) {
return 42;
}
};
struct Foo1 : Base1<Foo1> {
using value_type = int;
};
template <typename Derived>
struct Base2 {
auto baz() {
return typename Derived::value_type {};
}
};
struct Foo2 : Base2<Foo2> {
using value_type = int;
};
The first one fails to compile while the second compiles. My intuition says that they should both either compile or both not compile. Now, if we replace the auto
in Base2
with the explicit type:
template <typename Derived>
struct Base3 {
typename Derived::value_type baz() {
return typename Derived::value_type {};
}
};
struct Foo3 : Base3<Foo3> {
using value_type = int;
};
it no longer compiles; but I don't really see what's the big difference here. What's going on?
Note: This came up in David S. Hollman's lightning talk, Thoughts on Curiously Recurring Template Pattern, in C++-Now 2019.
CRTP may be used to implement "compile-time polymorphism", when a base class exposes an interface, and derived classes implement such interface.
The curiously recurring template pattern (CRTP) is an idiom, originally in C++, in which a class X derives from a class template instantiation using X itself as a template argument. More generally it is known as F-bound polymorphism, and it is a form of F-bounded quantification.
The type Foo1
is complete only at the end of };
struct Foo1 : Base1<Foo1> {
// still incomplete
} /* now complete */;
But before Foo1
start to become defined, it must first instantiate the base class for the base class to be complete.
template <typename Derived>
struct Base1 {
// Here, no type are complete yet
// function declaration using a member of incomplete type
int baz(typename Derived::value_type) {
return 42;
}
};
Inside the base class body, no class are complete yet. You cannot use nested typename there. The declaration must all be valid when defining a class type.
Inside the body of member function, it's different.
Just like this code don't work:
struct S {
void f(G) {}
using G = int;
};
But this one is okay:
struct S {
void f() { G g; }
using G = int;
};
Inside the body of member functions, all types are considered complete.
So... why does the auto
return type works if it deduce to the type you cannot access?
auto
return type is indeed special, since it allows function with deduced return types to be forward declared, like this:
auto foo();
// later
auto foo() {
return 0;
}
So the deduction of auto can be used to defer usage of types in the declaration that would be otherwise incomplete.
If auto
was deduced instantaneously, types in the body of the function would not be complete as the specification imply, since it would have to instantiate the body of the function when defining the type.
As for parameter types, they are also part of the declaration of the function, so the derived class is still incomplete.
Although you cannot use the incomplete types, you can check if the deduced parameter type is really typename Derived::value_type
.
Even though the instantiated function recieve typename Derived::value_type
(when called with the right set of argument), it is only defined at the instantiation point. And at that point, the types are complete.
There's something analoguous to the auto return type but for parameter and that means a template:
template<typename T>
int baz(T) {
static_assert(std::is_same_v<typename Derived::value_type, T>)
return 42;
}
As long as you don't directly use the name from the incomplete type inside declarations, you'll be okay. You can use indirections such as templates or deduced return types and that will make the compiler happy.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With