std::forward 如何工作,特别是在传递左值/右值引用时?
我知道什么确实如此,以及何时使用它,但我仍然无法理解它是如何工作的。请尽可能详细地说明,并解释如果允许使用模板参数推导,std::forward
何时会不正确。
我的部分困惑是: “如果它有一个名称,它就是一个左值” - 如果是这样的话,为什么当我传递 thing&& 时
与 std::forward
的行为有所不同? xthing& x?
Possible Duplicate:
What are the main purposes of std::forward and which problems does it solve?
I know what it does and when to use it but I still can't wrap my head around how it works. Please be as detailed as possible and explain when std::forward
would be incorrect if it was allowed to use template argument deduction.
Part of my confusion is this:
"If it has a name, it's an lvalue" - if that's the case why does std::forward
behave differently when I pass thing&& x
vs thing& x
?
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(3)
我认为将
std::forward
解释为static_cast
令人困惑。我们对强制转换的直觉是,它将一种类型转换为其他类型——在本例中,它将转换为右值引用。它不是!所以我们正在用一种神秘的事物来解释另一种神秘的事物。这个特定的演员阵容是由 Xeo 的答案中的表格定义的。但问题是:为什么?所以这是我的理解:假设我想向您传递一个
std::vector; v
您应该将其作为数据成员_v
存储在数据结构中。简单(且安全)的解决方案是始终将向量复制到其最终目的地。因此,如果您通过中间函数(方法)执行此操作,则应将该函数声明为采用引用。 (如果您将其声明为按值获取向量,那么您将执行额外的完全不必要的复制。)如果您手中有一个左值,那么这一切都很好,但是右值呢?假设该向量是调用函数
makeAndFillVector()
的结果。如果您执行直接赋值:编译器将移动向量而不是复制它。但是,如果您引入中介
set()
,则有关参数的右值性质的信息将丢失,并且将创建副本。为了避免这种复制,您需要“完美转发”,这将每次都产生最佳代码。如果给定一个左值,您希望函数将其视为左值并制作一个副本。如果给定一个右值,您希望函数将其视为右值并移动它。
通常,您可以通过分别为左值和右值重载函数
set()
来完成此操作:但现在假设您正在编写一个接受
T
并调用的模板函数>set()
和T
(不用担心我们的set()
仅针对向量定义)。诀窍在于,您希望此模板在使用左值实例化模板函数时调用set()
的第一个版本,并在使用右值初始化时调用第二个版本。首先,这个函数的签名应该是什么?答案是这样的:
根据您调用此模板函数的方式,类型
T
将会神奇地以不同的方式推导出来。如果使用左值调用它:向量
v
将通过引用传递。但如果您使用右值调用它:(匿名)向量将通过右值引用传递。因此,C++11 魔法是有目的地设置的,以便在可能的情况下保留参数的右值性质。
现在,在 PerfectSet 内部,您希望将参数完美地传递给
set()
的正确重载。这就是std::forward
必要的地方:如果没有 std::forward,编译器将不得不假设我们想要通过引用传递 t。为了让自己相信这是真的,请将此代码
与以下代码进行比较:
如果您没有显式转发
t
,编译器必须防御性地假设您可能会再次访问 t 并选择左值引用版本的集合。但如果您转发t
,编译器将保留它的右值性,并且将调用set()
的右值引用版本。这个版本移动了t
的内容,也就是说原来的变成了空。这个答案比我最初假设的要长得多;-)
I think the explanation of
std::forward
asstatic_cast<T&&>
is confusing. Our intuition for a cast is that it converts a type to some other type -- in this case it would be a conversion to an rvalue reference. It's not! So we are explaining one mysterious thing using another mysterious thing. This particular cast is defined by a table in Xeo's answer. But the question is: Why? So here's my understanding:Suppose I want to pass you an
std::vector<T> v
that you're supposed to store in your data structure as data member_v
. The naive (and safe) solution would be to always copy the vector into its final destination. So if you are doing this through an intermediary function (method), that function should be declared as taking a reference. (If you declare it as taking a vector by value, you'll be performing an additional totally unnecessary copy.)This is all fine if you have an lvalue in your hand, but what about an rvalue? Suppose that the vector is the result of calling a function
makeAndFillVector()
. If you performed a direct assignment:the compiler would move the vector rather than copy it. But if you introduce an intermediary,
set()
, the information about the rvalue nature of your argument would be lost and a copy would be made.In order to avoid this copy, you need "perfect forwarding", which would result in optimal code every time. If you're given an lvalue, you want your function to treat it as an lvalue and make a copy. If you're given an rvalue, you want your function to treat it as an rvalue and move it.
Normally you would do it by overloading the function
set()
separately for lvalues and rvalues:But now imagine that you're writing a template function that accepts
T
and callsset()
with thatT
(don't worry about the fact that ourset()
is only defined for vectors). The trick is that you want this template to call the first version ofset()
when the template function is instantiated with an lvalue, and the second when it's initialized with an rvalue.First of all, what should the signature of this function be? The answer is this:
Depending on how you call this template function, the type
T
will be somewhat magically deduced differently. If you call it with an lvalue:the vector
v
will be passed by reference. But if you call it with an rvalue:the (anonymous) vector will be passed by rvalue reference. So the C++11 magic is purposefully set up in such a way as to preserve the rvalue nature of arguments if possible.
Now, inside perfectSet, you want to perfectly pass the argument to the correct overload of
set()
. This is wherestd::forward
is necessary:Without std::forward the compiler would have to assume that we want to pass t by reference. To convince yourself that this is true, compare this code:
to this:
If you don't explicitly forward
t
, the compiler has to defensively assume that you might be accessing t again and chose the lvalue reference version of set. But if you forwardt
, the compiler will preserve the rvalue-ness of it and the rvalue reference version ofset()
will be called. This version moves the contents oft
, which means that the original becomes empty.This answer turned out much longer than what I initially assumed ;-)
首先我们来看看
std::forward
按照标准做了什么:§20.2.3 [forward] p2
(其中
T
是显式指定的模板参数,t
code> 是传递的参数。)现在记住引用折叠规则:(
无耻地从 这个答案。)
然后让我们看一个想要使用完美的类转发:
现在是一个示例调用:
我希望这个分步答案可以帮助您和其他人了解
std::forward
的工作原理。First, let's take a look at what
std::forward
does according to the standard:§20.2.3 [forward] p2
(Where
T
is the explicitly specified template parameter andt
is the passed argument.)Now remember the reference collapsing rules:
(Shamelessly stolen from this answer.)
And then let's take a look at a class that wants to employ perfect forwarding:
And now an example invocation:
I hope this step-by-step answer helps you and others understand just how
std::forward
works.它之所以有效,是因为当调用完美转发时,类型 T 不是值类型,它也可能是引用类型。
例如:
因此,
forward
可以简单地查看显式类型 T 来查看您真正传递的内容。当然,如果我记得的话,执行此操作的确切实现并不简单,但这就是信息所在。当您引用命名右值引用时,那么它确实是一个左值。然而,
forward
通过上面的方式检测到它实际上是一个右值,并正确返回一个要转发的右值。It works because when perfect forwarding is invoked, the type T is not the value type, it may also be a reference type.
For example:
As such,
forward
can simply look at the explicit type T to see what you really passed it. Of course, the exact implementation of doing this is non-trival, if I recall, but that's where the information is.When you refer to a named rvalue reference, then that is indeed an lvalue. However,
forward
detects through the means above that it is actually an rvalue, and correctly returns an rvalue to be forwarded.