Is a leading return type method declaration, using decltype, compatible with a trailing return type definition?

337 Views Asked by At

When using decltype, is it permissible to use the traditional leading return type syntax in a declaration:

  decltype(expr) foo();

and then use C++11 trailing return type syntax in the definition?

  auto foo() -> decltype(expr) { /*...*/ }

I think the answer is yes, as C++11 8.3.5p1 (leading return type) and 8.3.5p2 (trailing return type) both seem to yield the same final type description regardless of which side the return type appears on, and 7.1.6.2p4 (decltype) doesn't seem to have anything that would change that. Furthermore, the note in 9.3p9 shows an example of declaring a member using a typedef, but explains it could not be defined with a typedef, implying that they do not have to use exactly the same syntactic conventions.

However, I've got an example where both Clang and GCC do not think it is allowed, although MSVC does (godbolt link):

// The class must be a template for the problem to happen.
template <typename T>
struct Class {
  // This has to be inside the class for the problem to happen.
  int dataMember;

  // All is fine if I use trailing return type here.
  //auto method() -> decltype(dataMember);

  // But it fails with leading return type in the declaration.
  decltype(dataMember) method();
};

// Definition uses trailing return type.
template <typename T>
auto Class<T>::method() -> decltype(dataMember) {
  return 3;
}

Clang 16.0.0 says:

<source>:16:16: error: return type of out-of-line definition of 'Class::method' differs from that in the declaration
auto Class<T>::method() -> decltype(dataMember) {
               ^
<source>:11:24: note: previous declaration is here
  decltype(dataMember) method();
  ~~~~~~~~~~~~~~~~~~~~ ^
1 error generated.
Compiler returned: 1

GCC 13.1 says, somewhat more informatively but dubiously:

<source>:16:6: error: no declaration matches 'decltype (((Class<T>*)this)->Class<T>::dataMember) Class<T>::method()'
   16 | auto Class<T>::method() -> decltype(dataMember) {
      |      ^~~~~~~~
<source>:11:24: note: candidate is: 'decltype (Class<T>::dataMember) Class<T>::method()'
   11 |   decltype(dataMember) method();
      |                        ^~~~~~
<source>:3:8: note: 'struct Class<T>' defined here
    3 | struct Class {
      |        ^~~~~
Compiler returned: 1

MSVC 19 is happy with this code, even if I add code that instantiates Class and calls method (although it then issues a warning about electing to inline the method, which is a little weird).

I'm inclined to believe that these two signatures (one with leading return type and one with trailing return type) are in fact supposed to be equivalent in C++, and that furthermore I am allowed to use leading return type in the declaration and trailing return type in the definition, despite these two compilers' objections, because for both of them, the problem only happens if the class is a template and dataMember is declared inside it (suggesting the compilers might both have similar bugs).

Have I overlooked some provision that makes this not permissible?

(Tangent: Why would I want the declaration and definition to differ in this regard? Normally I would not, but this is the output of a code transformation tool. The declaration is in the original code, which usually uses leading return type, and which I want to only minimally change, while the definition is generated, and the trailing return type syntax is very convenient in that context due to all class members being in scope there.)

Clarification: In the example above, dataMember has type int (because I wanted it to be minimal). But this evidently causes the answer to depend on CWG2064 because it is not a dependent type. My intended focus is the general case, including dependent types. So, the ideal answer would instead primarily answer for the case where int has been replaced with T (godbolt), with the degenerate case of int treated separately (if necessary).

2

There are 2 best solutions below

11
Artyer On

Neither gcc nor clang implement CWG2064.

This means since dataMember "involves a template parameter" (what clang calls "instantiation-dependent") by virtue of being a member of the current-instantiation, decltype(dataMember) is a "unique dependent type".

[temp.type]p4 (without wording changed by DR2064):

If an expression e involves a template parameter, decltype(e) denotes a unique dependent type. Two such decltype-specifiers refer to the same type only if their expressions are equivalent ([temp.over.link]).

[temp.over.link]p5:

Two expressions involving template parameters are considered equivalent if two function definitions containing the expressions would satisfy the one-definition rule, [...]

[class.mfct.non.static]p2:

When an id-expression ([expr.prim.id]) that is neither part of a class member access syntax ([expr.ref]) nor the unparenthesized operand of the unary & operator ([expr.unary.op]) is used where the current class is X ([expr.prim.this]), if name lookup ([basic.lookup]) resolves the name in the id-expression to a non-static non-type member of some class C, and if either the id-expression is potentially evaluated or C is X or a base class of X, the id-expression is transformed into a class member access expression ([expr.ref]) using (*this) as the postfix-expression to the left of the . operator.

So at your declaration decltype(dataMember) method();, dataMember refers to the same thing as Class::dataMember because there is no this (therefore there is no current class), but at your definition template <typename T> auto Class<T>::method() -> decltype(dataMember), dataMember is (*this).dataMember.
Even though they have the same tokens, these expressions are not equivalent because of the ODR [basic.def.odr]p(14.5):

In each such definition, corresponding names, looked up according to [basic.lookup], shall refer to the same entity, [...]

((*this).dataMember is not the same entity as Class<T>::dataMember)

You can see this is the case if you make sure there is no (*this). transformation:

template <typename T>
struct Class {
  int dataMember;

  decltype(std::type_identity_t<Class<T>>::dataMember) method();
};

template <typename T>
auto Class<T>::method() -> decltype(std::type_identity_t<Class<T>>::dataMember) {
  return 3;
}

If gcc and clang were to implement CWG2064, decltype(dataMember) would no longer be a dependent type and would simply be int, so this would work.

You would run into the same issue if you were to make the type of dataMember dependent:

template<typename T>
struct Class {
  T dataMember;

  decltype(dataMember) method();
};

template <typename T>
auto Class<T>::method() -> decltype(dataMember) {
  return 3;
} // Error: return type is different
6
Scott McPeak On

The answer by Artyer appears consistent with how Clang and GCC have implemented things. As there is still discussion about whether it's the correct interpretation of the language standard, I'm holding off on accepting it for the moment.

But I want to state my understanding of that answer and invite correction:

  1. Normally, leading return type and trailing return type declarations are compatible. Nothing in the standard directly or intentionally prohibits using one for a declaration and the other for a definition.

  2. However, when decltype is used in a template context, an incompatibility can arise due to subtleties of decltype semantics. Specifically:

    • In the leading return position, this is not in scope, so decltype(dataMember) means decltype(Class<T>::dataMember). (I have my doubts about this because [expr.prim.this]p2 talks about the "innermost class scope" and we are still inside a class scope, right?)

    • In the trailing return position, this is in scope, so decltype(dataMember) "is transformed into" decltype(this->dataMember), per [class.mfct.non.static]p2.

    • These are not equivalent because Class<T>::dataMember refers to a member declaration (a compile-time notion) while this->dataMember refers to an object (a run-time notion). (This claim is a point of contention. But the GCC error message seems to focus on this difference.)

    • For the example in my question, the behavior depends on the language change CWG2064 because the type of dataMember is manifestly int, potentially obviating the above points.

    • If dataMember had been declared as type T, then the incompatibility would persist even after CWG2064. (In retrospect, I wish I had done that because the concrete int type allows the core issue I care about to be bypassed.)

  3. Consequently, one cannot, in general, move a return type specified using decltype from the leading to trailing return positions and maintain compatibility between declaration and definition. Hence the answer to the question asked in the title, "Is a leading return type method declaration, using decltype, compatible with a trailing return type definition?", is not in general.