Trailing return types allow to simplify code in these two scenarios:
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 { /* ... */ }
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?
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.
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
whereD
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-listT
”, the type of the declarator-id inD
is “derived-declarator-type-list noexcept(opt) function of(
parameter-declaration-clause)
cv-qualifier-seq(opt) ref-qualifier(opt) returningT
”, 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).
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With