std::forward 的主要用途是什么?它解决了哪些问题?
在完美转发中,std::forward
用于将命名右值引用 t1
和 t2
转换为未命名右值引用。这样做的目的是什么?如果我们离开 t1
& ,这将如何影响被调用的函数 inner
t2
作为左值?
template <typename T1, typename T2>
void outer(T1&& t1, T2&& t2)
{
inner(std::forward<T1>(t1), std::forward<T2>(t2));
}
In perfect forwarding, std::forward
is used to convert the named rvalue references t1
and t2
to unnamed rvalue references. What is the purpose of doing that? How would that affect the called function inner
if we leave t1
& t2
as lvalues?
template <typename T1, typename T2>
void outer(T1&& t1, T2&& t2)
{
inner(std::forward<T1>(t1), std::forward<T2>(t2));
}
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(8)
您必须了解转发问题。您可以详细阅读整个问题,但我会总结一下。
基本上,给定表达式
E(a, b, ... , c)
,我们希望表达式f(a, b, ... , c)
为相等的。在C++03中,这是不可能的。有很多尝试,但都在某些方面失败了。最简单的是使用左值引用:
但这无法处理临时值(右值):
f(1, 2, 3);
,因为它们无法绑定到左值引用。下一次尝试可能是:
这解决了上述问题,因为“
const X&
绑定到所有内容”,包括左值和右值,但这会导致一个新问题。现在它不允许E
具有非const
参数:第三次尝试接受 const 引用,但随后
const_cast
是>const
away:这接受所有值,可以传递所有值,但可能导致未定义的行为:
最终的解决方案正确处理所有内容......但代价是无法维护。您提供了
f
的重载,以及 const 和非常量的所有 组合:N 个参数需要 2N 个组合,这是一场噩梦。我们希望自动执行此操作。
(这实际上是我们让编译器在 C++11 中为我们做的事情。)
在 C++11 中,我们有机会解决这个问题。 一种解决方案修改了现有类型的模板推导规则,但这可能会破坏大量代码。 所以我们必须另想办法。
解决方案是改用新添加的rvalue-references;我们可以在推导右值引用类型时引入新规则并创建任何所需的结果。毕竟,我们现在不可能破解代码。
如果给出对引用的引用(注意引用是一个包含性术语,既表示
T&
又表示T&&
),我们使用以下规则来计算结果类型:或者以表格形式:
接下来,使用模板参数推导:如果参数是左值 A,我们为模板参数提供对 A 的左值引用。否则,我们正常推导。这提供了所谓的通用参考(术语转发参考现已成为官方参考)。
为什么这有用?因为组合起来,我们保持了跟踪类型的值类别的能力:如果它是左值,则我们有一个左值引用参数,否则我们有一个右值引用参数。
在代码中:
最后一件事是“转发”变量的值类别。请记住,一旦进入函数内部,参数就可以作为左值传递给任何东西:
这是不好的。 E 需要获得与我们相同的值类别!解决方案是这样的:
这有什么作用?假设我们在
deduce
函数中,并且我们已经传递了一个左值。这意味着T
是A&
,因此静态转换的目标类型是A& &&
,或只是A&
。由于x
已经是A&
,所以我们什么都不做,只剩下一个左值引用。当我们传递一个右值时,
T
是A
,因此静态转换的目标类型是A&&
。转换结果产生一个右值表达式,它不能再传递给左值引用。我们维护了参数的值类别。将它们放在一起为我们提供了“完美转发”:
当
f
收到左值时,E
收到左值。当f
收到右值时,E
收到右值。完美的。当然,我们希望摆脱丑陋的事物。
static_cast
很神秘,而且记起来很奇怪;让我们创建一个名为forward
的实用函数,它执行相同的操作:You have to understand the forwarding problem. You can read the entire problem in detail, but I'll summarize.
Basically, given the expression
E(a, b, ... , c)
, we want the expressionf(a, b, ... , c)
to be equivalent. In C++03, this is impossible. There are many attempts, but they all fail in some regard.The simplest is to use an lvalue-reference:
But this fails to handle temporary values (rvalues):
f(1, 2, 3);
, as those cannot be bound to an lvalue-reference.The next attempt might be:
Which fixes the above problem because "
const X&
binds to everything", including both lvalues and rvalues, but this causes a new problem. It now fails to allowE
to have non-const
arguments:The third attempt accepts const-references, but then
const_cast
's theconst
away:This accepts all values, can pass on all values, but potentially leads to undefined behavior:
A final solution handles everything correctly...at the cost of being impossible to maintain. You provide overloads of
f
, with all combinations of const and non-const:N arguments require 2N combinations, a nightmare. We'd like to do this automatically.
(This is effectively what we get the compiler to do for us in C++11.)
In C++11, we get a chance to fix this. One solution modifies template deduction rules on existing types, but this potentially breaks a great deal of code. So we have to find another way.
The solution is to instead use the newly added rvalue-references; we can introduce new rules when deducing rvalue-reference types and create any desired result. After all, we cannot possibly break code now.
If given a reference to a reference (note reference is an encompassing term meaning both
T&
andT&&
), we use the following rule to figure out the resulting type:Or in tabular form:
Next, with template argument deduction: if an argument is an lvalue A, we supply the template argument with an lvalue reference to A. Otherwise, we deduce normally. This gives so-called universal references (the term forwarding reference is now the official one).
Why is this useful? Because combined we maintain the ability to keep track of the value category of a type: if it was an lvalue, we have an lvalue-reference parameter, otherwise we have an rvalue-reference parameter.
In code:
The last thing is to "forward" the value category of the variable. Keep in mind, once inside the function the parameter could be passed as an lvalue to anything:
That's no good. E needs to get the same kind of value-category that we got! The solution is this:
What does this do? Consider we're inside the
deduce
function, and we've been passed an lvalue. This meansT
is aA&
, and so the target type for the static cast isA& &&
, or justA&
. Sincex
is already anA&
, we do nothing and are left with an lvalue reference.When we've been passed an rvalue,
T
isA
, so the target type for the static cast isA&&
. The cast results in an rvalue expression, which can no longer be passed to an lvalue reference. We've maintained the value category of the parameter.Putting these together gives us "perfect forwarding":
When
f
receives an lvalue,E
gets an lvalue. Whenf
receives an rvalue,E
gets an rvalue. Perfect.And of course, we want to get rid of the ugly.
static_cast<T&&>
is cryptic and weird to remember; let's instead make a utility function calledforward
, which does the same thing:我认为有一个实现 std::forward 的概念代码可以帮助理解。这是 Scott Meyers 演讲中的幻灯片 An effective C++11/ 14 Sampler
代码中的函数
move
为std::move
。在那次演讲的前面有一个(有效的)实现。我发现 libstdc++ 中 std::forward 的实际实现,在文件 move.h 中,但它根本没有指导意义。从用户的角度来看,它的含义是 std::forward 是到右值的条件转换。如果我正在编写一个函数,该函数需要参数中的左值或右值,并且仅当它作为右值传入时才希望将其作为右值传递给另一个函数,那么它会很有用。如果我没有将参数包装在 std::forward 中,它将始终作为普通引用传递。
果然,它打印了
The code is based on an example from the previous提到的演讲。幻灯片 10,从开始的 15:00 左右开始。
I think to have a conceptual code implementing std::forward can help with the understanding. This is a slide from Scott Meyers talk An Effective C++11/14 Sampler
Function
move
in the code isstd::move
. There is a (working) implementation for it earlier in that talk. I found actual implementation of std::forward in libstdc++, in file move.h, but it is not at all instructive.From a user's perspective, the meaning of it is that
std::forward
is a conditional cast to an rvalue. It can be useful if I am writing a function which expects either an lvalue or rvalue in a parameter and wants to pass it to another function as an rvalue only if it was passed in as an rvalue. If I did not wrap the parameter in std::forward, it would be always passed as a normal reference.Sure enough, it prints
The code is based on an example from the previously mentioned talk. Slide 10, at about 15:00 from the start.
如果在表达式中使用命名右值引用,它实际上是左值(因为您通过名称引用对象)。考虑以下示例:
现在,如果我们像这样调用
outer
,我们希望将 17 和 29 转发到 #2,因为 17 和 29 是整数文字,因此是右值。但由于表达式
inner(t1,t2);
中的t1
和t2
是左值,因此您将调用 #1 而不是 #2 。这就是为什么我们需要使用std::forward
将引用转回未命名引用。因此,outer
中的t1
始终是左值表达式,而forward(t1)
可能是右值表达式,具体取决于T1
。如果 T1 是左值引用,则后者只是左值表达式。仅当outer 的第一个参数是左值表达式时,T1
才会被推断为左值引用。If you use a named rvalue reference in an expression it is actually an lvalue (because you refer to the object by name). Consider the following example:
Now, if we call
outer
like thiswe would like 17 and 29 to be forwarded to #2 because 17 and 29 are integer literals and as such rvalues. But since
t1
andt2
in the expressioninner(t1,t2);
are lvalues, you'd be invoking #1 instead of #2. That's why we need to turn the references back into unnamed references withstd::forward
. So,t1
inouter
is always an lvalue expression whileforward<T1>(t1)
may be an rvalue expression depending onT1
. The latter is only an lvalue expression ifT1
is an lvalue reference. AndT1
is only deduced to be an lvalue reference in case the first argument to outer was an lvalue expression.如果实例化后,
T1
属于char
类型,并且T2
属于类,则您希望传递t1
> 每个副本和t2
每个const
引用。好吧,除非inner()
根据非const
引用获取它们,也就是说,在这种情况下您也想这样做。尝试编写一组
outer()
函数,在没有右值引用的情况下实现此目的,从而推导出从inner()
类型传递参数的正确方法。我认为你需要其中的 2^2 个东西,相当大的模板元东西来推断参数,并且需要大量的时间来使所有情况都正确。然后有人提出了一个
inner()
,它接受每个指针的参数。我认为现在是 3^2。 (或者 4^2。见鬼,我懒得去思考 const 指针是否会产生影响。)然后想象一下你想对五个参数执行此操作。或者七个。
现在你知道为什么一些聪明的人想出了“完美转发”:它让编译器为你做这一切。
If, after instantiating,
T1
is of typechar
, andT2
is of a class, you want to passt1
per copy andt2
perconst
reference. Well, unlessinner()
takes them per non-const
reference, that is, in which case you want to do so, too.Try to write a set of
outer()
functions which implement this without rvalue references, deducing the right way to pass the arguments frominner()
's type. I think you'll need something 2^2 of them, pretty hefty template-meta stuff to deduce the arguments, and a lot of time to get this right for all cases.And then someone comes along with an
inner()
that takes arguments per pointer. I think that now makes 3^2. (Or 4^2. Hell, I can't be bothered to try to think whetherconst
pointer would make a difference.)And then imagine you want to do this for a five parameters. Or seven.
Now you know why some bright minds came up with "perfect forwarding": It makes the compiler do all this for you.
尚未明确的一点是
static_cast
也能正确处理const T&
。程序:
生成:
请注意,“f”必须是模板函数。如果它只是定义为“void f(int&& a)”,则这是行不通的。
A point that hasn't been made crystal clear is that
static_cast<T&&>
handlesconst T&
properly too.Program:
Produces:
Note that 'f' has to be a template function. If it's just defined as 'void f(int&& a)' this doesn't work.
可能值得强调的是,转发必须与具有转发/通用引用的外部方法一起使用。允许单独使用前向作为以下语句,但除了造成混乱之外没有任何好处。标准委员会可能想要禁用这种灵活性,否则为什么我们不直接使用 static_cast 呢?
在我看来,移动和前进是设计模式,是引入 r 值引用类型后的自然结果。我们不应该假设一个方法被正确使用来命名它,除非禁止错误的使用。
It may be worthwhile to emphasize that forward has to be used in tandem with an outer method with forwarding/universal reference. Using forward by itself as the following statements is allowed, but does no good other than causing confusion. The standard committee may want to disable such flexibility otherwise why don't we just use static_cast instead?
In my opinion, move and forward are design patterns which are natural outcomes after r-value reference type is introduced. We should not name a method assuming it is correctly used unless incorrect usage is forbidden.
从另一个角度来看,在处理通用引用赋值中的右值时,可能需要按原样保留变量的类型。例如,
使用
std::forward
,我们确保z
与x
具有完全相同的类型。此外,
std::forward
不会影响左值引用:z
仍然与x
具有相同的类型。因此,回到您的情况,如果内部函数有
int&
和int&&
两个重载,您需要传递z
这样的变量> 作业不是y
1。示例中的类型可以通过以下方式评估:
From another viewpoint, when dealing with rvalues in a universal reference assignment, it may be desirable to preserve the type of a variable as it is. For example
Using
std::forward
, we ensuredz
exactly has the same type asx
.Moreover,
std::forward
doesn't affect lvalue references:Still
z
has the same type asx
.So, back to your case, if the inner function has two overloads for
int&
andint&&
, you want to pass variables likez
assignment noty
one.The types in the example can be assessed via:
std::forward
保留变量的原始 l/r 值类型,打印输出为:
std::forward
preserves variable's original l/r value typethe print out is: