Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

CRTP and c++1y return type deduction

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.

like image 669
Morwenn Avatar asked Nov 10 '13 17:11

Morwenn


People also ask

Can you use auto as a return type in C++?

In C++14, you can just use auto as a return type.

Can a function have an auto return type?

C++: “auto” return type deduction The “auto” keyword used to say the compiler: “The return type of this function is declared at the end”. In C++14, the compiler deduces the return type of the methods that have “auto” as return type. Restrictions: All returned values must be of the same type.

Which C++ added feature of Auto for return type of function?

Since the compiler already has to deduce the return type from the return statement, in C++14, the auto keyword was extended to do function return type deduction. This works by using the auto keyword in place of the function's return type.


2 Answers

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();
    }
};
like image 113
dyp Avatar answered Oct 23 '22 12:10

dyp


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";
}
like image 34
Aaron McDaid Avatar answered Oct 23 '22 13:10

Aaron McDaid