使用前进的优点

Ste*_*eng 411 c++ c++-faq rvalue-reference perfect-forwarding c++11

在完美转发中,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));
}
Run Code Online (Sandbox Code Playgroud)

GMa*_*ckG 754

您必须了解转发问题.你可以详细阅读整个问题,但我会总结一下.

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


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

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

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

下一次尝试可能是:

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

这解决了上面的问题,但翻转了翻牌.现在它不允许E有非const参数:

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

第三次尝试接受const引用,但接着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));
}
Run Code Online (Sandbox Code Playgroud)

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

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

最终解决方案正确处理所有事情......代价是无法维护.您提供的过载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);
Run Code Online (Sandbox Code Playgroud)

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

(这实际上是我们让编译器在C++ 11中为我们做的.)


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

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

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

"[给定]一个类型TR,它是对类型T的引用,尝试创建类型"左值引用cv TR"创建类型"左值引用T",同时尝试创建类型"rvalue引用到cv TR"创建TR类型".

或者以表格形式:

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)
Run Code Online (Sandbox Code Playgroud)

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

为什么这有用?因为组合我们保持跟踪类型的值类别的能力:如果它是左值,我们有一个左值引用参数,否则我们有一个rvalue-reference参数.

在代码中:

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

int i;
deduce(i); // deduce<int&>(int& &&) -> deduce<int&>(int&)
deduce(1); // deduce<int>(int&&)
Run Code Online (Sandbox Code Playgroud)

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

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
Run Code Online (Sandbox Code Playgroud)

那不好.E需要获得与我们相同的价值类别!解决方案是这样的:

static_cast<T&&>(x);
Run Code Online (Sandbox Code Playgroud)

这是做什么的?考虑一下我们在deduce函数内部,并且我们已经传递了一个左值.这意味着T是一个A&,所以静态强制转换的目标类型是A& &&,或者只是A&.既然x已经是A&,我们什么都不做,并留下左值参考.

当我们传递一个右值时,TA,所以静态强制转换的目标类型是A&&.转换导致rvalue表达式,表达式不能再传递给左值引用.我们维护了参数的值类别.

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

template <typename A>
void f(A&& a)
{
    E(static_cast<A&&>(a)); 
}
Run Code Online (Sandbox Code Playgroud)

f接收到一个左值,E得到一个左值.当f收到一个右值时,E得到一个右值.完善.


当然,我们想要摆脱丑陋.static_cast<T&&>要记住是神秘而奇怪的; 让我们改为调用一个实用函数forward,它执行相同的操作:

std::forward<A>(a);
// is the same as
static_cast<A&&>(a);
Run Code Online (Sandbox Code Playgroud)

  • @David:`std :: move`应该在没有显式模板参数的情况下调用,并且总是产生一个rvalue,而`std :: forward`可能最终为.当你知道你不再需要这个值并想把它移到别处时使用`std :: move`,使用`std :: forward`来根据传递给你的函数模板的值来做. (24认同)
  • 在这里使用`forward`或`move`有什么不同吗?或者它只是一个语义差异? (5认同)
  • 感谢首先从具体的例子开始并激发问题; 很有帮助! (5认同)
  • 对于问题陈述,您的最后一次尝试是不正确的:它将 const 值作为非 const 转发,因此根本不转发。另请注意,在第一次尝试中,“const int i”将被接受:“A”被推导为“const int”。失败是针对右值文字的。另请注意,对于“deduced(1)”的调用,x 是“int&amp;&amp;”,而不是“int”(完美转发永远不会进行复制,如果“x”是按值参数,则会进行复制)。只是“T”是“int”。“x”在转发器中计算为左值的原因是因为命名右值引用变成了左值表达式。 (2认同)

use*_*610 56

我认为有一个实现std :: forward的概念代码可以添加到讨论中.这是来自Scott Meyers的一篇幻灯片,讲述了一个有效的C++ 11/14采样器

实现std :: forward的概念代码

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

从用户的角度来看,它的含义是std::forward对rvalue的条件转换.如果我正在编写一个函数,该函数需要参数中的左值或右值,并且只有当它作为右值传入时才想将它作为右值传递给另一个函数.如果我没有将参数包装在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));
}
Run Code Online (Sandbox Code Playgroud)

果然,它打印出来

std::string& version
std::string&& version
Run Code Online (Sandbox Code Playgroud)

该代码基于前面提到的谈话中的示例.从开始大约15:00滑10.

  • 哇,很好的解释。我从这个视频开始:https://www.youtube.com/watch?v=srdwFMZY3Hg,但读了你的答案后,终于感觉到了。:) (3认同)
  • 您的第二个链接最终指向了完全不同的地方。 (2认同)

sel*_*tze 29

在完美转发中,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));
}
Run Code Online (Sandbox Code Playgroud)

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

void inner(int &,  int &);  // #1
void inner(int &&, int &&); // #2
Run Code Online (Sandbox Code Playgroud)

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

outer(17,29);
Run Code Online (Sandbox Code Playgroud)

我们希望将17和29转发到#2,因为17和29是整数文字和rvalues.但由于t1t2表达式inner(t1,t2);是左值,你可以调用#1,而不是#2.这就是为什么我们需要将引用转回到未命名的引用中std::forward.因此,t1in outer始终是左值表达式,而forward<T1>(t1)可能是rvalue表达式,具体取决于T1.如果T1是左值引用,则后者仅是左值表达式.并且T1只有在外部的第一个参数是左值表达式的情况下,才推导出它是左值引用.

  • 这是一种淡化的解释,但却是一个做得很好且实用的解释。人们应该首先阅读这个答案,然后根据需要进行更深入的研究 (3认同)

sbi*_*sbi 11

如果我们将t1和t2作为左值,那将如何影响被调用的函数内部?

如果在实例化之后,T1是类型char,并且T2是类,则要传递t1每个副本和t2每个const引用.好吧,除非inner()按照非const引用的方式使用它们,也就是说,在这种情况下你也想这样做.

尝试编写一组outer()函数,在没有右值引用的情况下实现它,推导出从inner()类型传递参数的正确方法.我认为你需要2 ^ 2个东西,相当大的模板元素来推断论证,并且有很多时间来适应所有情况.

然后有人带着一个inner()每个指针接受一个参数.我认为现在是3 ^ 2.(或者4 ^ 2.太好了,我不能费心去思考const指针是否有所作为.)

然后想象你想要为五个参数做这个.或七.

现在你知道为什么一些聪明的头脑想出了"完美的转发":它让编译器为你做这一切.


Bil*_*man 6

还没有说清楚的一点是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);
}
Run Code Online (Sandbox Code Playgroud)

产生:

f(1)
int&&
f(a)
int&
f(const b)
const int&
f(a * b)
int&&
Run Code Online (Sandbox Code Playgroud)

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


小智 5

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

     std::forward<int>(1);
     std::forward<std::string>("Hello");
Run Code Online (Sandbox Code Playgroud)

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