使用 auto&& 完美转发 return 值
Perfect-forwarding a return value with auto&&
参考 C++ 模板:完整指南(第 2 版)中的这句话:
decltype(auto) ret{std::invoke(std::forward<Callable>(op),
std::forward<Args>(args)...)};
...
return ret;
Note that declaring ret
with auto&&
is not correct. As a
reference, auto&&
extends the lifetime of the returned value until
the end of its scope but not beyond the return
statement to the
caller of the function.
作者说 auto&&
不适合完美转发 return 值。但是,decltype(auto)
不也是对 xvalue/lvalue 的引用吗?
IMO,decltype(auto)
然后遇到同样的问题。那么,作者的意思是什么?
编辑:
上面的代码片段应该放在这个函数模板中。
template<typename Callable, typename... Args>
decltype(auto) call(Callable&& op, Args&&... args) {
// here
}
However, doesn't decltype(auto) also form a reference to xvalue/lvalue?
没有
decltype(auto)
的部分魔力在于它知道 ret
是一个左值,so it will not form a reference。
如果您编写 return (ret)
,它确实会解析为引用类型,并且您将返回对局部变量的引用。
tl;dr:decltype(auto)
并不总是与 auto&&
相同。
这里有两个推导。一个来自 return 表达式,一个来自 std::invoke
表达式。因为decltype(auto)
is deduced to be the declared type for unparenthesized id-expression,我们可以着重从std::invoke
表达式推导
引自[dcl.type.auto.deduct] paragraph 5:
If the placeholder is the decltype(auto)
type-specifier, T
shall be the placeholder alone. The type deduced for T
is determined as described in [dcl.type.simple], as though e
had been the operand of the decltype
.
并引用自[dcl.type.simple] paragraph 4:
For an expression e
, the type denoted by decltype(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 by e
. If there is no such entity, or if e
names a set of overloaded functions, the program is ill-formed;
otherwise, if e
is an xvalue, decltype(e)
is T&&
, where T
is the type of e
;
otherwise, if e
is an lvalue, decltype(e)
is T&
, where T
is the type of e
;
otherwise, decltype(e)
is the type of e
.
注意如果 e
是纯右值,则 decltype(e)
被推导为 T
而不是 T&&
。这是与auto&&
.
的区别
所以如果std::invoke(std::forward<Callable>(op), std::forward<Args>(args)...)
是纯右值,比如Callable
的return类型不是引用,即returning by value,ret
被推导为同一类型而不是引用,完美转发了returning的语义。
auto&&
始终是引用类型。另一方面,decltype(auto)
可以是引用类型或值类型,具体取决于使用的初始化程序。
由于return
语句中的ret
没有被括号括起来,call()
推导出的return类型只依赖于声明的类型实体 ret
的 以及 值类别 的 非 表达式 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;
}
如果Callable
returns by value,那么op
的调用表达式的值类别将是一个prvalue。在那种情况下:
decltype(auto)
会将 res
推断为 non-reference 类型(即值类型)。
auto&&
会推导出 res
作为引用类型。
如上所述,call()
的 return 类型的 decltype(auto)
与 res
的类型相同。因此,如果 auto&&
将被用于推断 res
的类型而不是 decltype(auto)
,那么 call()
的 return 类型将是对本地对象 ret
,在 call()
returns.
之后不存在
我有一个类似的问题,但具体是如何正确地 return ret
就像我们直接调用 invoke
而不是 call
.
在您显示的示例中,call(A, B)
没有 每个 A
具有相同的 return 类型的 std::invoke(A, B)
和 B
.
具体来说,当 invoke
return 是 T&&
,call
return 是 T&
。
你可以在这个例子中看到它(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
}
因此看来正确转发函数的 return 值的唯一方法是
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;
}
}
这是一个很大的陷阱(我是掉进去才发现的!)。
return T&&
的功能非常罕见,很容易在一段时间内未被发现。
参考 C++ 模板:完整指南(第 2 版)中的这句话:
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.
作者说 auto&&
不适合完美转发 return 值。但是,decltype(auto)
不也是对 xvalue/lvalue 的引用吗?
IMO,decltype(auto)
然后遇到同样的问题。那么,作者的意思是什么?
编辑:
上面的代码片段应该放在这个函数模板中。
template<typename Callable, typename... Args>
decltype(auto) call(Callable&& op, Args&&... args) {
// here
}
However, doesn't decltype(auto) also form a reference to xvalue/lvalue?
没有
decltype(auto)
的部分魔力在于它知道 ret
是一个左值,so it will not form a reference。
如果您编写 return (ret)
,它确实会解析为引用类型,并且您将返回对局部变量的引用。
tl;dr:decltype(auto)
并不总是与 auto&&
相同。
这里有两个推导。一个来自 return 表达式,一个来自 std::invoke
表达式。因为decltype(auto)
is deduced to be the declared type for unparenthesized id-expression,我们可以着重从std::invoke
表达式推导
引自[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
.
并引用自[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
.
注意如果 e
是纯右值,则 decltype(e)
被推导为 T
而不是 T&&
。这是与auto&&
.
所以如果std::invoke(std::forward<Callable>(op), std::forward<Args>(args)...)
是纯右值,比如Callable
的return类型不是引用,即returning by value,ret
被推导为同一类型而不是引用,完美转发了returning的语义。
auto&&
始终是引用类型。另一方面,decltype(auto)
可以是引用类型或值类型,具体取决于使用的初始化程序。
由于return
语句中的ret
没有被括号括起来,call()
推导出的return类型只依赖于声明的类型实体 ret
的 以及 值类别 的 非 表达式 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;
}
如果Callable
returns by value,那么op
的调用表达式的值类别将是一个prvalue。在那种情况下:
decltype(auto)
会将res
推断为 non-reference 类型(即值类型)。auto&&
会推导出res
作为引用类型。
如上所述,call()
的 return 类型的 decltype(auto)
与 res
的类型相同。因此,如果 auto&&
将被用于推断 res
的类型而不是 decltype(auto)
,那么 call()
的 return 类型将是对本地对象 ret
,在 call()
returns.
我有一个类似的问题,但具体是如何正确地 return ret
就像我们直接调用 invoke
而不是 call
.
在您显示的示例中,call(A, B)
没有 每个 A
具有相同的 return 类型的 std::invoke(A, B)
和 B
.
具体来说,当 invoke
return 是 T&&
,call
return 是 T&
。
你可以在这个例子中看到它(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
}
因此看来正确转发函数的 return 值的唯一方法是
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;
}
}
这是一个很大的陷阱(我是掉进去才发现的!)。
return T&&
的功能非常罕见,很容易在一段时间内未被发现。