使用前进的优点

时间:2010-08-27 06:59:04

标签: c++ c++11 rvalue-reference c++-faq perfect-forwarding

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

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

6 个答案:

答案 0 :(得分:719)

您必须了解转发问题。你可以read the entire problem in detail,但我会总结一下。

基本上,给定表达式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);
}

但是这无法处理临时值: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);
}

这解决了上述问题,但翻转了翻牌。它现在无法允许E具有非const参数:

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

第三次尝试接受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));
}

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

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

最终解决方案正确处理所有事情......代价是无法维护。您提供f的重载,以及所有 const和非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个参数需要2个 N 组合,这是一场噩梦。我们想自动执行此操作。

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


在C ++ 11中,我们有机会解决这个问题。 One solution modifies template deduction rules on existing types, but this potentially breaks a great deal of code.所以我们必须找到另一种方式。

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

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

  

“[given]类型TR是对类型T的引用,尝试创建类型”对cv TR的左值引用“创建类型”左值引用T“,同时尝试创建类型” rvue对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)

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

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

在代码中:

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

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

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

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

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

static_cast<T&&>(x);

这是做什么的?考虑一下我们在deduce函数中,我们已经传递了一个左值。这意味着TA&,因此静态广播的目标类型为A& &&,或仅A&。由于x已经是A&,我们什么都不做,并留下左值参考。

当我们传递右值时,TA,因此静态广播的目标类型为A&&。强制转换导致rvalue表达式,它不能再传递给左值引用。我们维护了参数的值类别。

将这些放在一起可以让我们“完美转发”:

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

f收到左值时,E会获得左值。当f收到右值时,E会获得右值。完美。


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

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

答案 1 :(得分:49)

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

的幻灯片

conceptual code implementing std::forward

代码中的函数movestd::move。在那次谈话中,它有一个(工作)实现。我在文件move.h中找到了actual implementation of std::forward in libstdc++,但它根本没有用处。

从用户的角度来看,它的含义是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));
}

果然,打印

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

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

答案 2 :(得分:23)

  

在完美转发中,std :: forward用于将命名的右值引用t1和t2转换为未命名的右值引用。这样做的目的是什么?如果我们离开t1&amp; t2是左值?

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

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

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

现在,如果我们像这样打电话给outer

outer(17,29);

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

答案 3 :(得分:11)

  

如果我们离开t1&amp; t2是左值?

如果在实例化后,T1属于char类型且T2属于某个类,则您希望每个副本传递t1t2const个引用。好吧,除非inner()根据非const引用获取它们,也就是说,在这种情况下,您也希望这样做。

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

然后有人带着inner()来获取每个指针的参数。我认为现在是3 ^ 2。 (或者4 ^ 2.太好了,我不能试图去思考const指针是否会产生影响。)

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

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

答案 4 :(得分:3)

尚未明确表达的观点是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);
}

产地:

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

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

答案 5 :(得分:1)

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

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

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