Perfect-forwarding a return value with auto&&
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.
I had a similar question, but specific to how to properly return ret
as if we called invoke
directly instead of call
.
In the example you show, call(A, B)
does not have the same return type of std::invoke(A, B)
for every A
and B
.
Specifically, when invoke
returns an T&&
, call
returns a T&
.
You can see it in this example (wandbox link)
#include <type_traits>
#include <iostream>
struct PlainReturn {
template<class F, class Arg>
decltype(auto) operator()(F&& f, Arg&& arg) {
decltype(auto) ret = std::forward<F>(f)(std::forward<Arg>(arg));
return ret;
}
};
struct ForwardReturn {
template<class F, class Arg>
decltype(auto) operator()(F&& f, Arg&& arg) {
decltype(auto) ret = std::forward<F>(f)(std::forward<Arg>(arg));
return std::forward<decltype(ret)>(ret);
}
};
struct IfConstexprReturn {
template<class F, class Arg>
decltype(auto) operator()(F&& f, Arg&& arg) {
decltype(auto) ret = std::forward<F>(f)(std::forward<Arg>(arg));
if constexpr(std::is_rvalue_reference_v<decltype(ret)>) {
return std::move(ret);
} else {
return ret;
}
}
};
template<class Impl>
void test_impl(Impl impl) {
static_assert(std::is_same_v<int, decltype(impl([](int) -> int {return 1;}, 1))>, "Should return int if F returns int");
int i = 1;
static_assert(std::is_same_v<int&, decltype(impl([](int& i) -> int& {return i;}, i))>, "Should return int& if F returns int&");
static_assert(std::is_same_v<int&&, decltype(impl([](int&& i) -> int&& { return std::move(i);}, 1))>, "Should return int&& if F returns int&&");
}
int main() {
test_impl(PlainReturn{}); // Third assert fails: returns int& instead
test_impl(ForwardReturn{}); // First assert fails: returns int& instead
test_impl(IfConstexprReturn{}); // Ok
}
So it appears that the only way to properly forward the return value of a function is by doing
decltype(auto) operator()(F&& f, Arg&& arg) {
decltype(auto) ret = std::forward<F>(f)(std::forward<Arg>(arg));
if constexpr(std::is_rvalue_reference_v<decltype(ret)>) {
return std::move(ret);
} else {
return ret;
}
}
This is quite a pitfall (which I discovered by falling into it!).
Functions which return T&&
are rare enough that this can easily go undetected for a while.
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 deduceres
as a non-reference type (i.e., value type).auto&&
would deduceres
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&&
.