使用std :: forward的主要目的是什么,它可以解决哪些问题?


438

在完善的转发,std::forward是用来转换指定的右值引用t1,并t2以不具名rvalue引用。这样做的目的是什么?inner如果我们将t1&保留t2为左值,对调用的函数有何影响?

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

Answers:


789

您必须了解转发问题。您可以详细阅读整个问题,但我将进行总结。

基本上,给定表达式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包含非常量参数:

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

第三次尝试接受常量引用,但随后const_cast的的const路程:

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和non-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中,我们有机会解决此问题。一种解决方案修改了现有类型的模板推导规则,但这可能会破坏大量代码。因此,我们必须寻找另一种方法。

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

如果给一个参考基准(附注引用是一个包罗万象的术语,意思都T&T&&),我们使用以下规则计算出结果类型:

“给定类型T的引用类型T,尝试创建类型“对cv TR的左值引用”会创建类型“对T的左值引用”,而尝试创建对类型T的“右值引用” 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的左值引用。否则,我们通常进行推论。这提供了所谓的通用参考(术语“ 转发参考”现在是正式参考)。

为什么这有用?因为结合起来,我们可以保持跟踪类型的值类别的能力:如果是左值,则有一个左值引用参数,否则有一个右值引用参数。

在代码中:

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&,我们什么也不做,只剩下一个左值引用。

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

将它们放在一起可以使我们“完美转发”:

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
不会f是函数,而不是表达式?
Michael Foukarakis 2010年

1
关于问题陈述,您的最后一次尝试是不正确的:它将const值作为非const转发,因此根本不转发。另请注意,在第一次尝试中,const int i将被接受:A被推导为const int。失败是针对右值文字的。还要注意,对于deduced(1),x是int&&,而不是int(完美的转发永远不会复制,就像x将其作为按值参数一样)。仅仅Tintx在转发器中求值为左值的原因是因为命名的右值引用成为左值表达式。
Johannes Schaub-litb

5
使用forwardmove此处有什么区别吗?还是仅仅是语义上的差异?
0x499602D2

28
@David:std::move应在没有显式模板参数的情况下调用,并且始终会导致右值,而std::forward最终可能会出现右值。使用std::move时,你知道你不再需要的价值,并希望到别处移动它,用std::forward做根据传递给你的函数模板中的值。
GManNickG 2013年

5
感谢您首先从具体示例入手,并激发问题的发展;非常有帮助!
ShreevatsaR

61

我认为有一个实现std :: forward的概念代码可以增加讨论的范围。这是斯科特·迈耶斯(Scott Meyers)讲的《有效的C ++ 11/14采样器》中的一张幻灯片

实现std :: forward的概念代码

move代码中的功能是std::move。在该演讲的前面有一个(有效的)实现。我在libstdc ++中的move.h文件中找到了std :: forward的实际实现,但是这完全没有启发性。

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

果然,它会打印

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

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


2
您的第二个链接最终指向完全不同的地方。
法拉普

33

在理想的转发中,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));
}

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

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

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

outer(17,29);

我们希望将17和29转发给#2,因为17和29是整数文字,并且是这样的右值。但是由于表达式中的t1和是左值,因此您将调用#1而不是#2。因此,我们需要使用将引用转换回未命名的引用。因此,in 始终是一个左值表达式,而取决于则可能是一个右值表达式。后者仅是左值表达式,如果t2inner(t1,t2);std::forwardt1outerforward<T1>(t1)T1T1是左值引用。并且T1只有在external的第一个参数是左值表达式的情况下,才推断出它是左值引用。


这是一种淡化的解释,但做得很好且功能齐全。人们应该先阅读此答案,然后再深入了解是否需要
NicoBerrogorry

11

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

如果,实例化后,T1是类型的char,并且T2是一类的,你要传递t1每复制和t2const参考。好吧,除非inner()每个非const引用都采用它们,也就是说,在这种情况下,您也要这样做。

尝试编写一组在outer()没有右值引用的情况下实现此功能的函数,从而推断出从中传递参数的正确方法inner()的类型。我认为您将需要2 ^ 2的东西,可以使用大量的模板元数据来推论参数,并且需要大量时间才能在所有情况下都做到这一点。

然后有人来了inner(),每个指针都接受参数。我认为现在等于3 ^ 2。(或4 ^ 2。该死,我不必费心去思考const指针是否会有所作为。)

然后想象您要为五个参数执行此操作。或七个。

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


5

还没有明确说明的一点是也可以正确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 && a)”,则此方法无效。


好点,所以静态转换中的T &&也遵循参考崩溃规则,对吗?
巴尼

3

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

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

我认为,前进和前进是设计模式,是引入r值参考类型后的自然结果。除非禁止不正确的用法,否则我们不应以正确使用的方法命名。


我认为C ++委员会认为“正确”使用语言习语,甚至定义“正确”用法的责任不成立(尽管他们当然可以提供指导)。为此,虽然一个人的老师,老板和朋友可能有责任以一种或另一种方式指导他们,但我相信C ++委员会(因此是标准)没有这种义务。
SirGuy

是的,我刚刚阅读了N2951,并且我同意标准委员会没有义务对功能的使用添加不必要的限制。但是,仅在库文件或标准文档(23.2.5转发/移动帮助器)中仅看到它们的定义,这两个功能模板的名称(移动和前进)确实确实有些令人困惑。该标准中的示例肯定有助于理解该概念,但是添加更多注释以使情况更清楚可能会很有用。
科林
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.