9

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.

einpoklum
  • 118,144
  • 57
  • 340
  • 684
  • 1
    @PicaudVincent: Thanks, looks interesting. Too bad the font/background contrasts on that blog is poor. – einpoklum Aug 27 '19 at 20:06

2 Answers2

5

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.

Guillaume Racicot
  • 39,621
  • 9
  • 77
  • 141
  • I don't think this answers my question. `Foo2::baz()` effectively has `Derived::value_type` in its declaration - through the use of `auto`. So why is it more complete than when the use of `Derived::value_type` is explicit? – einpoklum Aug 27 '19 at 15:43
  • Well, if it's okay to delay return type determination until instantiation, why is that not ok for a parameter type? – einpoklum Aug 27 '19 at 18:12
  • Your actual answer is the single paragraph starting with "auto return type is indeed special". – einpoklum Aug 27 '19 at 18:26
  • 1
    @einpoklum your question ask why does one compile and why the other don't, so I have to start by explaining where and why the types are complete or incomplete. I cleaned up the answer, it should be better now. – Guillaume Racicot Aug 27 '19 at 18:42
1

tl;dr: Because of a special consideration for an auto return type.

(Shortened version of @GuillaumeRacicot's answer)

The compiler needs to have a declaration of all of the types in member function's signatures when a template class is defined (except if for pointers or references). Derived::value_type is not known, for which reason Base1 and Base3 don't compile.

But there's a special exception for an auto return type: It's as though you're forward-declaring that auto return type, and can actually define in when the member is actually instantiated. That's why Base2 does compile.

einpoklum
  • 118,144
  • 57
  • 340
  • 684