12

I came across this code snipped involving trailing return types and inheritance.

The following minimal example compiles fine with g++, not with clang

struct Base {};

int foo(Base&) {
    return 42;
}

struct Derived : public Base {
    auto bar() -> decltype(foo(*this)) {
        return foo(*this);
    }
};

int main()
{
    Derived derived;
    derived.bar();  

    return 0;
}

However, if we change auto bar() -> decltype(foo(*this)) to decltype(auto) bar() (c++14 extension), the code compiles also with clang. Link to godbolt https://godbolt.org/z/qf_k6X .

Can anyone explain me

  • how auto bar() -> decltype(return expression) differs from decltype(auto) bar()
  • why the behavior between compilers differs
  • what the right implementation is?
nyedidikeke
  • 6,899
  • 7
  • 44
  • 59

2 Answers2

5

This is a gcc bug, the trailing return type isn't within a complete-class context [class.mem]

A complete-class context of a class is a

  • function body,
  • default argument,
  • noexcept-specifier ([except.spec]),
  • contract condition, or
  • default member initializer

We see that a complete class is needed for the derived to base conversion from [conv.ptr]

A prvalue of type “pointer to cv D”, where D is a complete class type, can be converted to a prvalue of type “pointer to cv B”, where B is a base class of D.

and [dcl.init.ref]

“cv1 T1” is reference-compatible with “cv2 T2” if a prvalue of type “pointer to cv2 T2” can be converted to the type “pointer to cv1 T1” via a standard conversion sequence. In all cases where the reference-compatible relationship of two types is used to establish the validity of a reference binding and the standard conversion sequence would be ill-formed, a program that necessitates such a binding is ill-formed.

On the other hand, a function body is within a complete-class context and thus the derived to base conversion is well-formed. The return type involving a placeholder type (decltype(auto)) is valid as long as it is already deduced before an expression using it.

For a possible workaround in C++11, you may use

auto bar() -> decltype(foo(std::declval<Base&>()))
{
    return foo(*this);
}

provided you know that you want to call it with Base.

Community
  • 1
  • 1
Passer By
  • 19,325
  • 6
  • 49
  • 96
  • This just made me wonder: Is this simply an oversight or is there a particular reason why this should not work? If there is a reason this should not work, then why is it allowed in a noexcept specifier which, since C++17, is just as much part of the type of the function as the return type!? – Michael Kenzel Apr 26 '19 at 11:27
  • @MichaelKenzel https://stackoverflow.com/questions/54767088/is-there-a-specific-reason-why-a-trailing-return-type-is-not-a-complete-class-co – Passer By Apr 26 '19 at 13:57
  • @PasserBy thanks for the pointer. So it does seem that that's kinda a defect in the standard…!? – Michael Kenzel Apr 26 '19 at 14:04
  • @MichaelKenzel Most likely for the `noexcept` case, yes. – Passer By Apr 26 '19 at 14:13
  • Then how are covariant return types of the same class expected to compile? – curiousguy Apr 26 '19 at 17:19
  • Do you mean `Derived& f();`? Forming the reference is legal, just not having it be converted. Converting within the function body is also allowed. – Passer By Apr 26 '19 at 18:34
1

I think Clang is wrong to reject this:

Regarding the return type of a function definition, the C++14 standard says this:

[dcl.fct]/9]

Types shall not be defined in return or parameter types. The type of a parameter or the return type for a function definition shall not be an incomplete class type (possibly cv-qualified) unless the function is deleted (8.4.3) or the definition is nested within the member-specification for that class (including definitions in nested classes defined within the class).

In your example the definition of bar is nested within the member-specification of the class Derived. So this is allowed and GCC, ICC and MSVC get this right.

On the other hand decltype(auto) works because a deduced return type is not actually deduced until the signature of the function is needed. And in your case, this happens when you call bar() in main. At that point of time the class Derived is a completely defined type. Clang gets this right.

Note that even using auto instead of decltype(auto) will work for your example. See Demo on godbolt.

P.W
  • 26,289
  • 6
  • 39
  • 76
  • Your bolded sentence applies to `struct A { A f(); };`, not that the class is complete at that point. – Passer By Apr 26 '19 at 06:12
  • @PasserBy: The class is not complete. But the definition of `bar` (and not just its declaration) is nested within the member specification. Actually the quote from `[class.mem]/2` is not needed. Just thought it would be a good to include the quote. Do you think it is distracting? – P.W Apr 26 '19 at 06:16
  • I meant, that bolded sentence doesn't make the class complete at the nested definition, it just allows for the specific case of returning the incomplete class. The declaration is still not in a complete class context. – Passer By Apr 26 '19 at 06:18
  • @PasserBy: Yes, I agree. I think the second quote causes the misunderstanding as people will think that I am saying that the class is complete at that point. I have removed it. – P.W Apr 26 '19 at 06:20