c++c++11language-lawyerreturn-typetrailing-return-type

What are the name lookup and type simplification rules for trailing return types?


Trailing return types allow to simplify code in these two scenarios:

  1. Returning a type defined inside the class from one of the class's member functions:

    struct X
    {
        using foo = int;
        foo f();
    };
    
    // pre-C++11
    X::foo X::f()      { /* ... */ }
    
    // trailing, doesn't require `X::` before `foo`
    auto X::f() -> foo { /* ... */ }
    
  2. Returning a complicated type, such as a function pointer type:

    // pre-C++11
    int(*g(float))(int) { /* ... */ }
    
    // trailing, easier to read
    auto f(float) -> int(*)(int) { /* ... */ }
    

I am trying to find the relevant parts of Standard that explain how the above two simplifications work. I've looked at [basic.lookup] and grepped for trailing-return, but couldn't find anything straightforward that explained how the above transformations work.

Have I missed it?

What parts of the Standard explain the above trailing-return-type simplifications?


Solution

  • For #1, see C++17 [basic.lookup.qual]/3:

    In a declaration in which the declarator-id is a qualified-id, names used before the qualified-id being declared are looked up in the defining namespace scope; names following the qualified-id are looked up in the scope of the member's class or namespace.

    An ordinary leading return type precedes the declarator-id, namely X::f so it is looked up at namespace scope. A trailing return type follows it, so it is looked up in class scope.

    For #2, observe that the syntax for trailing-return-type from [dcl.decl]/4 is:

    -> type-id

    and according to [dcl.fct]/2, that type is the function's return type.

    If you were to use a leading return type, the determination of the function's return type would have to be determined recursively by [dcl.fct]/1:

    In a declaration T D where D has the form

    D1 ( parameter-declaration-clause ) cv-qualifier-seq(opt) ref-qualifier(opt) noexcept-specifier(opt) attribute-specifier-seq(opt)

    and the type of the contained declarator-id in the declaration T D1 is “derived-declarator-type-list T”, the type of the declarator-id in D is “derived-declarator-type-list noexcept(opt) function of (parameter-declaration-clause) cv-qualifier-seq(opt) ref-qualifier(opt) returning T”, where ...

    Here, T represents a decl-specifier-seq. If you had a typedef-name that denoted int(*)(int), say, FPII, then you could just use that:

    FPII g(float);
    

    But if you want to do it the hard way, we have to find T and D1 such that when the derived-declarator-type-list, i.e., the sequence of type transformations D1 would inflict on T according to the syntactic form of D1, are applied to "function of int returning T", the result is "function of float returning pointer to (function of int returning int)".

    This will be satisfied if the derived-declarator-type-list is "function of float returning pointer to", and T is int. The declarator D1 must therefore have the syntactic form * declarator-id (float) in order to yield said derived-declarator-type-list. We have to add an extra pair of parentheses in order to get the binding correct in the overall declaration.

    There is no "transformation" going on here from the trailing return type to a leading return type. Instead, the trailing return type just lets you specify the return type directly, whereas the leading return type is interpreted by this algorithm of recursively unwrapping the declarator. While this makes sense under the principle of "declaration follows usage", it tends to be a bit difficult for humans to grasp intuitively, including very experienced C++ programmers. And especially so when we have to do it in reverse (write down the declaration, instead of interpreting an existing one).