[英]Perfect-forwarding a return value with auto&&
考虑一下C++ 模板中的这句话:完整指南(第 2 版) :
decltype(auto) ret{std::invoke(std::forward<Callable>(op), std::forward<Args>(args)...)}; ... return ret;
请注意,用
auto&&
声明ret
是不正确的。 作为参考,auto&&
将返回值的生命周期延长到其范围的末尾,但不会超出函数调用者的return
语句。
作者说auto&&
不适合完美转发返回值。 但是, decltype(auto)
不也形成了对 xvalue/lvalue 的引用吗? IMO, decltype(auto)
然后遇到同样的问题。 那么,作者的意义何在?
编辑:
上面的代码片段应该放在这个函数模板中。
template<typename Callable, typename... Args>
decltype(auto) call(Callable&& op, Args&&... args) {
// here
}
这里有两个扣除。 一个来自 return 表达式,一个来自std::invoke
表达式。 因为decltype(auto)
被推导为未加括号的 id-expression 的声明类型,我们可以专注于从std::invoke
表达式推导。
引自[dcl.type.auto.deduct] 第 5 段:
如果占位符是
decltype(auto)
类型说明符,则T
应单独作为占位符。 为T
推导的类型如 [dcl.type.simple] 中所述确定,就好像e
是decltype
的操作数一样。
对于表达式
e
,decltype(e)
表示的类型定义如下:
如果
e
是命名结构化绑定 ([dcl.struct.bind]) 的未加括号的 id 表达式,则decltype(e)
是结构化绑定声明规范中给出的引用类型;否则,如果
e
是未加括号的id 表达式或未加括号的类成员访问,则decltype(e)
是e
命名的实体的类型。 如果没有这样的实体,或者如果e
命名了一组重载函数,则程序是病式的;否则,如果
e
是一个 xvalue,则decltype(e)
是T&&
,其中T
是e
的类型;否则,如果
e
是左值,则decltype(e)
是T&
,其中T
是e
的类型;否则,
decltype(e)
是e
的类型。
注意decltype(e)
被推导为T
而不是T&&
如果e
是纯右值。 这是与auto&&
的区别。
所以如果std::invoke(std::forward<Callable>(op), std::forward<Args>(args)...)
是纯右值,例如Callable
的返回类型不是引用,即按值返回, ret
被推导为同类型而不是引用,完美转发了按值返回的语义。
但是,decltype(auto) 不也形成了对 xvalue/lvalue 的引用吗?
不。
decltype(auto)
的部分魔力在于它知道ret
是左值,因此它不会形成引用。
如果您编写了return (ret)
,它确实会解析为引用类型,并且您将返回对局部变量的引用。
tl;dr: decltype(auto)
并不总是与auto&&
相同。
auto&&
始终是引用类型。 另一方面, decltype(auto)
可以是引用类型或值类型,具体取决于所使用的初始化程序。
由于return
语句中的ret
没有被括号包围,因此call()
的推导返回类型仅取决于实体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
按值返回,则op
的调用表达式的值类别将为纯右值。 在这种情况下:
decltype(auto)
会将res
推断为非引用类型(即值类型)。auto&&
会将res
推断为引用类型。 如上所述, call()
的返回类型中的decltype(auto)
只会导致与res
相同的类型。 因此,如果使用auto&&
而不是decltype(auto)
来推断res
的类型,则call()
的返回类型将是对本地对象ret
的引用,该对象在call()
返回后不存在。
我有一个类似的问题,但具体到如何正确返回ret
就像我们直接invoke
而不是call
一样。
在您显示的示例中, call(A, B)
, B) 对于每个A
和B
都没有与std::invoke(A, B)
相同的返回类型。
具体来说,当invoke
返回T&&
时, call
返回T&
。
你可以在这个例子中看到它( wandbox 链接)
#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
}
所以看起来正确转发函数返回值的唯一方法是做
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;
}
}
这是一个相当大的陷阱(我是掉进去才发现的)。
返回T&&
的函数很少见,很容易在一段时间内不被发现。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.