C++ 使用 forward 的优点

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/3582001/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-28 13:11:22  来源:igfitidea点击:

Advantages of using forward

c++c++11rvalue-referencec++-faqperfect-forwarding

提问by Steveng

In perfect forwarding, std::forwardis used to convert the named rvalue references t1and t2to unnamed rvalue references. What is the purpose of doing that? How would that affect the called function innerif we leave t1& t2as lvalues?

在完美转发中,std::forward用于转换命名的右值引用t1t2未命名的右值引用。这样做的目的是什么?inner如果我们将t1&保留t2为左值,这将如何影响被调用的函数?

template <typename T1, typename T2>
void outer(T1&& t1, T2&& t2) 
{
    inner(std::forward<T1>(t1), std::forward<T2>(t2));
}

回答by GManNickG

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 expression f(a, b, ... , c)to be equivalent. In C++03, this is impossible. There are many attempts, but they all fail in some regard.

基本上,给定表达式E(a, b, ... , c),我们希望表达式f(a, b, ... , c)是等价的。在 C++03 中,这是不可能的。有很多尝试,但它们都在某些方面失败了。



The simplest is to use an lvalue-reference:

最简单的是使用左值引用:

template <typename A, typename B, typename C>
void f(A& a, B& b, C& c)
{
    E(a, b, c);
}

But this fails to handle temporary values: f(1, 2, 3);, as those cannot be bound to an lvalue-reference.

但这无法处理临时值:f(1, 2, 3);,因为它们不能绑定到左值引用。

The next attempt might be:

下一次尝试可能是:

template <typename A, typename B, typename C>
void f(const A& a, const B& b, const C& c)
{
    E(a, b, c);
}

Which fixes the above problem, but flips flops. It now fails to allow Eto have non-const arguments:

这解决了上述问题,但触发器。它现在不允许E有非常量参数:

int i = 1, j = 2, k = 3;
void E(int&, int&, int&); f(i, j, k); // oops! E cannot modify these

The third attempt accepts const-references, but then const_cast's the constaway:

第三次尝试接受常量引用,但接下来const_castconst

template <typename A, typename B, typename C>
void f(const A& a, const B& b, const C& c)
{
    E(const_cast<A&>(a), const_cast<B&>(b), const_cast<C&>(c));
}

This accepts all values, can pass on all values, but potentially leads to undefined behavior:

这接受所有值,可以传递所有值,但可能会导致未定义的行为:

const int i = 1, j = 2, k = 3;
E(int&, int&, int&); f(i, j, k); // ouch! E can modify a const object!

A final solution handles everything correctly...at the cost of being impossible to maintain. You provide overloads of f, with allcombinations of const and non-const:

最终的解决方案可以正确处理所有事情……但代价是无法维护。您提供 的重载f,以及 const 和非常量的所有组合:

template <typename A, typename B, typename C>
void f(A& a, B& b, C& c);

template <typename A, typename B, typename C>
void f(const A& a, B& b, C& c);

template <typename A, typename B, typename C>
void f(A& a, const B& b, C& c);

template <typename A, typename B, typename C>
void f(A& a, B& b, const C& c);

template <typename A, typename B, typename C>
void f(const A& a, const B& b, C& c);

template <typename A, typename B, typename C>
void f(const A& a, B& b, const C& c);

template <typename A, typename B, typename C>
void f(A& a, const B& b, const C& c);

template <typename A, typename B, typename C>
void f(const A& a, const B& b, const C& c);

N arguments require 2Ncombinations, a nightmare. We'd like to do this automatically.

N 个参数需要 2 N 个组合,这是一场噩梦。我们希望自动执行此操作。

(This is effectively what we get the compiler to do for us in C++11.)

(这实际上是我们让编译器在 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.

在 C++11 中,我们有机会解决这个问题。一种解决方案修改现有类型的模板推导规则,但这可能会破坏大量代码。所以我们必须另辟蹊径。

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.

解决方案是改用新添加的rvalue-references;我们可以在推导右值引用类型时引入新规则并创建任何所需的结果。毕竟,我们现在不可能破解代码。

If given a reference to a reference (note reference is an encompassing term meaning both T&and T&&), we use the following rule to figure out the resulting type:

如果给定一个引用的引用(注意引用是一个包含T&和的包含术语T&&),我们使用以下规则来计算结果类型:

"[given] a type TR that is a reference to a type T, an attempt to create the type “lvalue reference to cv TR” creates the type “lvalue reference to T”, while an attempt to create the type “rvalue reference to cv TR” creates the type TR."

“[给定] 类型 TR,它是对类型 T 的引用,尝试创建类型“对 cv TR 的左值引用”会创建类型“对 T 的左值引用”,而尝试创建类型“对 cv TR 的左值引用” cv TR”创建类型 TR。”

Or in tabular form:

或以表格形式:

TR   R

T&   &  -> T&  // lvalue reference to cv TR -> lvalue reference to T
T&   && -> T&  // rvalue reference to cv TR -> TR (lvalue reference to T)
T&&  &  -> T&  // lvalue reference to cv TR -> lvalue reference to T
T&&  && -> T&& // rvalue reference to cv TR -> TR (rvalue reference to T)

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 referenceis now the official one).

接下来,使用模板参数推导:如果参数是左值 A,我们为模板参数提供对 A 的左值引用。否则,我们正常推导。这给出了所谓的通用引用(术语转发引用现在是官方引用)。

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:

在代码中:

template <typename T>
void deduce(T&& x); 

int i;
deduce(i); // deduce<int&>(int& &&) -> deduce<int&>(int&)
deduce(1); // deduce<int>(int&&)

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:

最后一件事是“转发”变量的值类别。请记住,一旦在函数内部,参数就可以作为左值传递给任何东西:

void foo(int&);

template <typename T>
void deduce(T&& x)
{
    foo(x); // fine, foo can refer to x
}

deduce(1); // okay, foo operates on x which has a value of 1

That's no good. E needs to get the same kind of value-category that we got! The solution is this:

那不好。E 需要获得与我们获得的相同类型的值类别!解决办法是这样的:

static_cast<T&&>(x);

What does this do? Consider we're inside the deducefunction, and we've been passed an lvalue. This means Tis a A&, and so the target type for the static cast is A& &&, or just A&. Since xis already an A&, we do nothing and are left with an lvalue reference.

这有什么作用?考虑我们在deduce函数内部,并且我们已经传递了一个左值。这意味着T是 a A&,因此静态转换的目标类型是A& &&,或者只是A&。由于x已经是一个A&,我们什么都不做,只剩下一个左值引用。

When we've been passed an rvalue, Tis A, so the target type for the static cast is A&&. 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.

当我们传递了一个右值Tis A,所以静态转换的目标类型是A&&。强制转换会产生一个右值表达式,表达式不能再传递给左值引用。我们维护了参数的值类别。

Putting these together gives us "perfect forwarding":

把这些放在一起给了我们“完美的转发”:

template <typename A>
void f(A&& a)
{
    E(static_cast<A&&>(a)); 
}

When freceives an lvalue, Egets an lvalue. When freceives an rvalue, Egets an rvalue. Perfect.

f接收到一个左值时,E得到一个左值。当f接收到一个右值时,E得到一个右值。完美的。



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 called forward, which does the same thing:

当然,我们想摆脱丑陋。static_cast<T&&>记起来很神秘而且很奇怪;让我们创建一个名为 的实用函数forward,它做同样的事情:

std::forward<A>(a);
// is the same as
static_cast<A&&>(a);

回答by user7610

I think to have a conceptual code implementing std::forward can add to the discussion. This is a slide from Scott Meyers talk An Effective C++11/14 Sampler

我认为有一个实现 std::forward 的概念代码可以添加到讨论中。这是 Scott Meyers 演讲幻灯片An Effective C++11/14 Sampler

conceptual code implementing std::forward

实现 std::forward 的概念代码

Function movein the code is std::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.

move代码中的函数是std::move. 在那次谈话的前面有一个(工作)实现。我在 libstdc++ 中找到了 std::forward 的实际实现,在文件 move.h 中,但它根本没有指导意义。

From a user's perspective, the meaning of it is that std::forwardis 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.

从用户的角度来看,它的含义std::forward是有条件地转换为右值。如果我正在编写一个函数,该函数期望参数中有一个左值或右值,并且希望将它作为右值传递给另一个函数,只有当它作为右值传入时,它会很有用。如果我没有将参数包装在 std::forward 中,它将始终作为普通引用传递。

#include <iostream>
#include <string>
#include <utility>

void overloaded_function(std::string& param) {
  std::cout << "std::string& version" << std::endl;
}
void overloaded_function(std::string&& param) {
  std::cout << "std::string&& version" << std::endl;
}

template<typename T>
void pass_through(T&& param) {
  overloaded_function(std::forward<T>(param));
}

int main() {
  std::string pes;
  pass_through(pes);
  pass_through(std::move(pes));
}

Sure enough, it prints

果然打印出来了

std::string& version
std::string&& version

The code is based on an example from the previously mentioned talk. Slide 10, at about 15:00 from the start.

该代码基于前面提到的演讲中的一个示例。幻灯片 10,大约在 15:00 开始。

回答by sellibitze

In perfect forwarding, std::forward is used to convert the named rvalue reference t1 and t2 to unnamed rvalue reference. What is the purpose of doing that? How would that effect the called function inner if we leave t1 & t2 as lvalue?

template <typename T1, typename T2> void outer(T1&& t1, T2&& t2) 
{
    inner(std::forward<T1>(t1), std::forward<T2>(t2));
}

在完美转发中,std::forward 用于将命名的右值引用 t1 和 t2 转换为未命名的右值引用。这样做的目的是什么?如果我们将 t1 & t2 保留为左值,这将如何影响被调用的函数内部?

template <typename T1, typename T2> void outer(T1&& t1, T2&& t2) 
{
    inner(std::forward<T1>(t1), std::forward<T2>(t2));
}

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:

如果在表达式中使用命名的右值引用,它实际上是一个左值(因为您通过名称引用对象)。考虑以下示例:

void inner(int &,  int &);  // #1
void inner(int &&, int &&); // #2

Now, if we call outerlike this

现在,如果我们称outer这样的

outer(17,29);

we would like 17 and 29 to be forwarded to #2 because 17 and 29 are integer literals and as such rvalues. But since t1and t2in the expression inner(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 with std::forward. So, t1in outeris always an lvalue expression while forward<T1>(t1)may be an rvalue expression depending on T1. The latter is only an lvalue expression if T1is an lvalue reference. And T1is only deduced to be an lvalue reference in case the first argument to outer was an lvalue expression.

我们希望 17 和 29 被转发到#2,因为 17 和 29 是整数文字,因此是右值。但是由于表达式中的t1和是左值,因此您将调用 #1 而不是 #2。这就是为什么我们需要将引用转换回未命名的引用。因此,in始终是一个左值表达式,而可能是一个取决于 的右值表达式。后者只是一个左值表达式 if是一个左值引用。And仅在外部的第一个参数是左值表达式的情况下被推导出为左值引用。t2inner(t1,t2);std::forwardt1outerforward<T1>(t1)T1T1T1

回答by sbi

How would that affect the called function inner if we leave t1 & t2 as lvalue?

如果我们将 t1 & t2 保留为左值,这将如何影响被调用的函数内部?

If, after instantiating, T1is of type char, and T2is of a class, you want to pass t1per copy and t2per constreference. Well, unless inner()takes them per non-constreference, that is, in which case you want to do so, too.

如果在实例化之后T1是 typechar并且T2是一个类,则您希望通过t1每个副本和t2每个const引用。好吧,除非inner()每个非const参考都采用它们,也就是说,在这种情况下,您也想这样做。

Try to write a set of outer()functions which implement this without rvalue references, deducing the right way to pass the arguments from inner()'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.

尝试编写一组在outer()没有右值引用的情况下实现这一点的函数,推导出从inner()的类型传递参数的正确方法。我认为你需要其中的 2^2 个东西,相当大的模板元东西来推断参数,并且需要大量时间来使所有情况都正确。

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 whether constpointer would make a difference.)

然后有人来了一个inner(),每个指针都接受参数。我认为现在是 3^2。(或 4^2。见鬼,我懒得去想const指针是否会有所作为。)

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.

现在您知道为什么一些聪明人提出“完美转发”了:它让编译器为您完成所有这些工作。

回答by lalawawa

A point that hasn't been made crystal clear is that static_cast<T&&>handles const T&properly too.
Program:

还没有说清楚的一点是static_cast<T&&>处理const T&得当。
程序:

#include <iostream>

using namespace std;

void g(const int&)
{
    cout << "const int&\n";
}

void g(int&)
{
    cout << "int&\n";
}

void g(int&&)
{
    cout << "int&&\n";
}

template <typename T>
void f(T&& a)
{
    g(static_cast<T&&>(a));
}

int main()
{
    cout << "f(1)\n";
    f(1);
    int a = 2;
    cout << "f(a)\n";
    f(a);
    const int b = 3;
    cout << "f(const b)\n";
    f(b);
    cout << "f(a * b)\n";
    f(a * b);
}

Produces:

产生:

f(1)
int&&
f(a)
int&
f(const b)
const int&
f(a * b)
int&&

Note that 'f' has to be a template function. If it's just defined as 'void f(int&& a)' this doesn't work.

请注意,'f' 必须是模板函数。如果它只是定义为 'void f(int&& a)' 这不起作用。

回答by colin

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?

值得强调的是,forward 必须与具有转发/通用引用的外部方法一起使用。允许单独使用 forward 作为以下语句,但除了引起混淆之外没有任何好处。标准委员会可能想要禁用这种灵活性,否则为什么我们不直接使用 static_cast 呢?

     std::forward<int>(1);
     std::forward<std::string>("Hello");

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.

在我看来,移动和前进是设计模式,这是引入 r 值引用类型后的自然结果。我们不应该假设一个方法被正确使用,除非禁止错误使用。