CRTP and c++1y return type deduction

女生的网名这么多〃 提交于 2019-12-21 03:31:07

问题


I was recently playing with CRTP when I came across something that surprised me when used with c++1y functions whose type is deduced. The following code works:

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

I assumed that the return type from Base<Derived>::foo was a decltype of the expression returned, but if I modify the functio foo like this:

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

This code does not work anymore, I get the following error (from 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'|

My questions are: Why doesn't it work? What could I possibly write to get the correct return type without relying on automatic return type deduction?

And, well... here is a live example.


回答1:


Why does the first example work (return type deduction)?

The definition of a member function of a class template is only implicitly instantiated when odr-used (or explicitly instantiated). That is, by deriving from Base<Derived>, you do not implicitly instantiate the function body. Hence, the return type is still not deduced yet.

At the (*) point of instantiation, Derived is complete, Derived::foo_impl is declared, and the return type deduction can succeed.

(*) not "the", but "certain points of instantiation". There are several.


Why doesn't the second example work (trailing-return-type)?

I assumed that the return type from Base<Derived>::foo was a decltype of the expression returned, but if I modify the function foo like this:

The trailing-return-type is part of the declaration of the member function; hence, it is part of the definition of the surrounding class, which is required to be instantiated when deriving from Base<Derived>. At this point, Derived is still incomplete, specifically Derived::foo_impl has not been declared yet.


What could I possibly write to get the correct return type without relying on automatic return type deduction?

Now this is tricky. I'd say this is not very clearly defined in the Standard, e.g. see this question.

Here's an example that demonstrates that clang++3.4 does not find the members of Derived inside Base<Derived>:

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

declval doesn't require a complete type, so the error message is that there's no foo_impl in Derived.


There's a hack, but I'm not sure if it's compliant:

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



回答2:


I've discovered a solution, perhaps not very pretty, but I think it's pretty standard compliant.

It's been pointed out that this is quite limited as it assumes that foo_impl can be implemented without accessing other parts of Derived or Base. Thanks @DyP. I've updated this answer with another approach.

Anyway, in terms of answering why the original code doesn't work, I defer to everyone else and to @Dyp's answer. I learned a lot, well described.

The basic issue, in layman's terms, (in my limited understanding!), is that when the compiler sees this line:

struct Derived:    public Base<Derived>

it immediately wants/needs to know some/all information about Base<Derived> even though it hasn't seen the following lines yet which define foo_impl.

A solution is to move foo_impl into another class called NotQuiteDerived. Then Derived inherits from this as well as from Base<...,...>. This allows us to put foo_impl before the introduction of Derived. Then we need a second template type parameter in Base. Anyway, the code can speak for itself ! :

I've changed this to a simpler, and maybe slightly better approach. Base doesn't need to see all of Derived, but the signature of foo_impl. This can be passed along with the CRTP parameter.


Another approach now that is more flexible than the last, as it allows foo_impl to have more access to Derived and to act as if it was effectively a method of Derived. We can declare foo_impl as a friend of Derived immediately before the struct Derived: .... This allows the implementation of foo_impl to see the full definition of everything, and it allows the Base to be given the return type of 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";
}


来源:https://stackoverflow.com/questions/19892479/crtp-and-c1y-return-type-deduction

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!