15

我最近在玩 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;
}

我假设 from 的返回类型Base<Derived>::foodecltype返回的表达式的 a ,但是如果我像这样修改函数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'|

我的问题是:为什么它不起作用?在不依赖自动返回类型扣除的情况下,我可以写什么来获得正确的返回类型?

而且,嗯……这是一个活生生的例子

4

2 回答 2

12

为什么第一个示例有效(返回类型扣除)?

类模板的成员函数的定义仅在使用 odr(或显式实例化)时才隐式实例化。也就是说,通过从 派生Base<Derived>,您不会隐式实例化函数体。因此,还没有推导出返回类型。

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

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


为什么第二个示例不起作用(尾随返回类型)?

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

trailing-return-type是成员函数声明的一部分;因此,它是周围类定义的一部分,在派生自 时需要对其进行实例化Base<Derived>。至此,Derived尚不完整,具体Derived::foo_impl还没有公布。


在不依赖自动返回类型扣除的情况下,我可以写什么来获得正确的返回类型?

现在这很棘手。我想说这在标准中没有很明确的定义,例如看这个问题

这是一个示例,说明 clang++3.4 找不到Derivedinside的成员Base<Derived>

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

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


有一个 hack,但我不确定它是否符合要求:

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();
    }
};
于 2013-11-10T18:21:56.757 回答
1

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

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

无论如何,在回答为什么原始代码不起作用方面,我尊重其他所有人和@Dyp 的回答。我学到了很多,描述得很好。

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

struct Derived:    public Base<Derived>

它立即想要/需要知道一些/所有信息,Base<Derived>即使它还没有看到以下定义的行foo_impl

一个解决方案是移动foo_impl到另一个名为NotQuiteDerived. 然后Derived继承自 this 以及 from Base<...,...>。这让我们可以foo_impl在介绍之前把Derived. 然后我们需要第二个模板类型参数Base。无论如何,代码可以自己说话!:

我已将其更改为更简单,甚至更好的方法。 Base不需要查看全部Derived,但需要查看 的签名foo_impl。这可以与 CRTP 参数一起传递。


现在的另一种方法比上一种方法更灵活,因为它允许foo_impl有更多的访问权限,Derived并且可以像有效地使用Derived. foo_impl我们可以Derivedstruct 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";
}
于 2013-11-10T21:58:45.960 回答