为什么只有这些CRTP模式之一可以编译?

时间:2019-08-27 15:08:19

标签: c++ auto crtp

使用CRTP模式考虑以下两段代码:

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;
};

第一个fails to compile,第二个compiles。我的直觉说,它们应该要么编译要么都不编译。现在,如果我们将auto中的Base2替换为显式类型:

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;但我看不出有什么大不同。发生了什么事?


注意:这是在C ++-Now 2019的David S. Hollman的闪电演讲Thoughts on Curiously Recurring Template Pattern中提出的。

2 个答案:

答案 0 :(得分:4)

类型Foo1仅在};的末尾完成

struct Foo1 : Base1<Foo1> {
    // still incomplete
} /* now complete */;

但是在开始定义Foo1之前,它必须首先实例化基类以使基类完整。

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; 
    }
};

在基类主体内,尚无完整的课程。您不能在那里使用嵌套的typename。定义类类型时,声明必须全部有效。

在成员函数的主体内部,这是不同的。

就像这样的代码不起作用:

struct S {
    void f(G) {}
    using G = int;
};

但是这个还可以:

struct S {
    void f() { G g; }
    using G = int;
};

在成员函数的主体内,所有类型都被视为完整的。

所以...如果auto返回类型推断出您无法访问的类型,为什么会起作用?

auto的返回类型确实很特殊,因为它允许对推定的返回类型的函数进行前向声明,例如:

auto foo();

// later

auto foo() {
    return 0;
}

因此,可以将auto的推论用于推迟声明中否则会不完整的类型的使用。

如果auto是瞬时推断的,则函数主体中的类型将不符合规范所暗示的,因为在定义类型时必须实例化函数主体。


对于参数类型,它们也是函数声明的一部分,因此派生类仍然不完整。

尽管不能使用不完整的类型,但可以检查推导的参数类型是否真的为typename Derived::value_type

即使实例化函数收到typename Derived::value_type(当使用正确的参数集调用时),它仅在实例化点定义。至此,类型已经完成。

有一个类似于自动返回类型的东西,但是对于参数来说,这意味着一个模板:

template<typename T>
int baz(T) {
    static_assert(std::is_same_v<typename Derived::value_type, T>) 
    return 42; 
}

只要您不直接使用声明中不完整类型中的名称,就可以了。您可以使用诸如模板或推导的返回类型之类的间接方式,这将使编译器感到满意。

答案 1 :(得分:0)

tl; dr:由于对auto返回类型的特殊考虑。

(@ GuillaumeRacicot的答案的简化版)

定义模板类时,编译器需要在成员函数的签名中声明所有类型(声明或指针除外)。 Derived::value_type未知,因此Base1Base3无法编译。

但是auto返回类型有一个特殊的例外:就像您正在向前声明auto返回类型一样,它实际上可以定义成员的实例化时间。这就是Base2 可以编译的原因。