Consider this quote from C++ Templates: The Complete Guide (2nd Edition):
decltype(auto) ret{std::invoke(std::forward<Callable>(op), std::forward<Args>(args)...)}; ... return ret;
Note that declaring
ret
withauto&&
is not correct. As a reference,auto&&
extends the lifetime of the returned value until the end of its scope but not beyond thereturn
statement to the caller of the function.
The author says that auto&&
is not appropriate for perfect-forwarding a return value. However, doesn't decltype(auto)
also form a reference to xvalue/lvalue?
IMO, decltype(auto)
then suffers from the same issue. Then, what's the point of the author?
EDIT:
The above code snippet shall go inside this function template.
template<typename Callable, typename... Args>
decltype(auto) call(Callable&& op, Args&&... args) {
// here
}
Perfect forwarding allows a template function that accepts a set of arguments to forward these arguments to another function whilst retaining the lvalue or rvalue nature of the original function arguments.
std::forward has a single use case: to cast a templated function parameter (inside the function) to the value category (lvalue or rvalue) the caller used to pass it. This allows rvalue arguments to be passed on as rvalues, and lvalues to be passed on as lvalues, a scheme called “perfect forwarding.”
When t is a forwarding reference (a function argument that is declared as an rvalue reference to a cv-unqualified function template parameter), this overload forwards the argument to another function with the value category it had when passed to the calling function.
There are two deductions here. One from the return expression, and one from the std::invoke
expression. Because decltype(auto)
is deduced to be the declared type for unparenthesized id-expression, we can focus on the deduction from the std::invoke
expression.
Quoted from [dcl.type.auto.deduct] paragraph 5:
If the placeholder is the
decltype(auto)
type-specifier,T
shall be the placeholder alone. The type deduced forT
is determined as described in [dcl.type.simple], as thoughe
had been the operand of thedecltype
.
And quoted from [dcl.type.simple] paragraph 4:
For an expression
e
, the type denoted bydecltype(e)
is defined as follows:
if
e
is an unparenthesized id-expression naming a structured binding ([dcl.struct.bind]),decltype(e)
is the referenced type as given in the specification of the structured binding declaration;otherwise, if
e
is an unparenthesized id-expression or an unparenthesized class member access,decltype(e)
is the type of the entity named bye
. If there is no such entity, or ife
names a set of overloaded functions, the program is ill-formed;otherwise, if
e
is an xvalue,decltype(e)
isT&&
, whereT
is the type ofe
;otherwise, if
e
is an lvalue,decltype(e)
isT&
, whereT
is the type ofe
;otherwise,
decltype(e)
is the type ofe
.
Note decltype(e)
is deduced to be T
instead of T&&
if e
is a prvalue. This is the difference from auto&&
.
So if std::invoke(std::forward<Callable>(op), std::forward<Args>(args)...)
is a prvalue, for example, the return type of Callable
is not a reference, i.e. returning by value, ret
is deduced to be the same type instead of a reference, which perfectly forwards the semantic of returning by value.
auto&&
is always a reference type. On the other hand, decltype(auto)
can be either a reference or a value type, depending on the initialiser used.
Since ret
in the return
statement is not surrounded by parenthesis, call()
's deduced return type only depends on the declared type of the entity ret
, and not on the value category of the expression ret
:
template<typename Callable, typename... Args>
decltype(auto) call(Callable&& op, Args&&... args) {
decltype(auto) ret{std::invoke(std::forward<Callable>(op),
std::forward<Args>(args)...)};
...
return ret;
}
If Callable
returns by value, then the value category of op
's call expression will be a prvalue. In that case:
decltype(auto)
will deduce res
as a non-reference type (i.e., value type). auto&&
would deduce res
as a reference type.As explained above, the decltype(auto)
at call()
's return type simply results in the same type as res
. Therefore, if auto&&
would have been used for deducing the type of res
instead of decltype(auto)
, call()
's return type would have been a reference to the local object ret
, which does not exist after call()
returns.
However, doesn't decltype(auto) also form a reference to xvalue/lvalue?
No.
Part of decltype(auto)
's magic is that it knows ret
is an lvalue, so it will not form a reference.
If you'd written return (ret)
, it would indeed have resolved to a reference type and you'd be returning a reference to a local variable.
tl;dr: decltype(auto)
is not always the same as auto&&
.
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