Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?

like image 274
Vittorio Romeo Avatar asked Oct 19 '19 14:10

Vittorio Romeo


2 Answers

IMO, you have two unrelated questions here, I'll try to answer the first one.
It is covered by [basic.scope.class]/1.5:

The potential scope of a declaration that extends to or past the end of a class definition also extends to the regions defined by its member definitions, even if the members are defined lexically outside the class (this includes static data member definitions, nested class definitions, member function definitions (including the member function body and any portion of the declarator part of such definitions which follows the declarator-id, including a parameter-declaration-clause and any default arguments ([dcl.fct.default]).

In the out-of-class definition of the member function

auto X::f() -> foo { /* ... */ }

the trailing-return-type is following the declarator-id X::f, so it is the potential scope for the class members, so the unqualified lookup finds X::foo when foo is mentioned there.

like image 137
Language Lawyer Avatar answered Sep 30 '22 13:09

Language Lawyer


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).

like image 25
Brian Bi Avatar answered Sep 30 '22 14:09

Brian Bi