CRTP和c ++ 1y返回类型推导

时间:2013-11-10 17:06:05

标签: c++ c++11 crtp c++14 return-type-deduction

我最近在玩CRTP的时候遇到的东西让我感到惊讶,因为它与c ++ 1y类型的函数一起被推断出来。以下代码有效:

template<typename Derived>
struct Base
{
    auto foo()
    {
        return static_cast<Derived*>(this)->foo_impl();
    }
};

struct Derived:
    public Base<Derived>
{
    auto foo_impl()
        -> int
    {
        return 0;
    }
};

int main()
{
    Derived b;
    int i = b.foo();
    (void)i;
}

我假设Base<Derived>::foo的返回类型是返回的表达式的decltype,但如果我像这样修改函数foo

auto foo()
    -> decltype(static_cast<Derived*>(this)->foo_impl())
{
    return static_cast<Derived*>(this)->foo_impl();
}

此代码不再有效,我收到以下错误(来自GCC 4.8.1):

||In instantiation of 'struct Base<Derived>':|
|required from here|
|error: invalid static_cast from type 'Base<Derived>* const' to type 'Derived*'|
||In function 'int main()':|
|error: 'struct Derived' has no member named 'foo'|

我的问题是:为什么不起作用?我可以写什么来获得正确的返回类型而不依赖于自动返回类型推导?

而且,嗯......这是一个live example

2 个答案:

答案 0 :(得分:12)

为什么第一个例子有效(返回类型推导)?

类模板的成员函数的定义仅在odr-used(或显式实例化)时被隐式实例化。也就是说,通过从Base<Derived>派生,您可以隐式实例化函数体。因此,返回类型仍未尚未推断

在实例化的(*)点,Derived完成,声明Derived::foo_impl,返回类型推断可以成功。

(*)不是“the”,而是“某些实例化点”。有几个。


为什么第二个例子不起作用(trailing-return-type)?

  

我认为Base<Derived>::foo的返回类型是decltype   返回的表达式,但如果我像这样修改函数foo

trailing-return-type 是成员函数声明的一部分;因此,它是周围类定义的一部分,在从Base<Derived>派生时需要对其进行实例化。此时,Derived仍然不完整,特别是Derived::foo_impl尚未宣布。


  

我可以写什么来获得正确的返回类型   依靠自动退货类型扣除?

现在这很棘手。我会说标准中没有明确定义,例如见this question

这是一个示例,演示了clang ++ 3.4在Derived内找不到Base<Derived>的成员:

template<typename Derived>
struct Base
{
    auto foo() -> decltype( std::declval<Derived&>().foo_impl() )
    {
        return static_cast<Derived*>(this)->foo_impl();
    }
};

declval不需要完整的类型,因此错误消息是foo_impl中没有Derived


有一个黑客,但我不确定它是否合规:

template<typename Derived>
struct Base
{
    template<class C = Derived>
    auto foo() -> decltype( static_cast<C*>(this)->foo_impl() )
    {
        static_assert(std::is_same<C, Derived>{}, "you broke my hack :(");
        return static_cast<Derived*>(this)->foo_impl();
    }
};

答案 1 :(得分:1)

我发现了一个解决方案,也许不是很漂亮,但我认为它非常符合标准。

有人指出,这是非常有限的,因为它假定foo_impl可以在不访问Derived或Base的其他部分的情况下实现。谢谢@DyP。我用另一种方法更新了这个答案。

无论如何,在回答原始代码不起作用的原因方面,我会推荐给其他所有人和@Dyp的答案。我学到了很多,很好描述。

基本问题,在外行人看来,(在我有限的理解中!),是当编译器看到这一行时:

struct Derived:    public Base<Derived>

它立即想要/需要知道有关Base<Derived>的部分/全部信息,即使它还没有看到定义foo_impl的以下行。

解决方案是将foo_impl移动到另一个名为NotQuiteDerived的类中。然后Derived继承了Base<...,...>以及foo_impl。这允许我们在引入Derived之前放置Base。然后我们需要Base中的第二个模板类型参数。无论如何,代码可以说明一切! :

我已将此更改为更简单,可能稍微更好的方法。 Derived不需要查看foo_impl的所有内容,而是foo_impl的签名。这可以与CRTP参数一起传递。


现在的另一种方法比上一种方法更灵活,因为它允许Derived拥有对Derived的更多访问权限,并且就像它实际上是foo_impl的方法一样。我们可以在Derived之前立即声明struct Derived: ...foo_impl的朋友。这允许实现Base以查看所有内容的完整定义,并允许foo_impl返回template<typename Derived, typename TypeOfTheFriendFunction> struct Base { auto foo() -> typename std::function<TypeOfTheFriendFunction> :: result_type { return foo_impl_as_friend(static_cast<Derived*>(this) /*, any other args here*/); } }; struct Derived; auto foo_impl_as_friend(Derived * This /*, any other args here*/) -> std::string; struct Derived: public Base<Derived, decltype(foo_impl_as_friend ) > { private: void method_that_foo_impl_needs() { } // Just to demonstrate that foo_impl can act as part of Derived friend decltype(foo_impl_as_friend) foo_impl_as_friend; }; auto foo_impl_as_friend(Derived *This) -> std::string { This -> method_that_foo_impl_needs(); return "a string"; } 的返回类型。

{{1}}