std::forward的主要目的是什么,它解决了哪些问题?

565

在完美转发中,使用 std::forward 将命名的右值引用 t1t2 转换为未命名的右值引用。这样做的目的是什么?如果我们将 t1t2 保留为左值,那么这会对被调用的函数 inner 产生什么影响呢?

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

请注意:您还可以编写 std::forward<decltype(t1)>(t1)decltype(t1)(t1),请参见c++ - Perfect forwarding in a lambda? - Stack Overflow - user202729
inner()的可能原型有哪些? - undefined
7个回答

1002
您需要了解转发问题。您可以详细阅读整个问题,但我会进行总结。
基本上,给定表达式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);
}

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

以上问题可以通过"const X&绑定到任何东西"来解决,包括左值和右值,但这会引起一个新的问题。现在无法允许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_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个参数需要2N种组合,这是一场噩梦。我们希望能够自动完成这个任务。(在C++11中,我们可以让编译器为我们完成这项工作。)


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

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

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

"[给定]一个类型TR,它是类型T的引用,试图创建类型“cv TR的左值引用”将创建类型“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&&)

最后一件事是“转发”变量的值类别。请记住,一旦在函数内部,参数可以作为lvalue传递给任何东西。
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&&。转换结果是一个右值表达式,不能再传递给左值引用。我们保持了参数的值类别。
将它们结合起来,就得到了“完美转发”的概念。
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);

6
在这里使用forwardmove是否有区别?还是只是语义上的差别? - David G
45
@David: std::move应该不带显式模板参数调用,并且始终产生一个右值,而std::forward可能会成为任何类型。当你知道你不再需要一个值并想将其移动到其他地方时,请使用std::move,根据传递给函数模板的值来使用std::forward - GManNickG
7
谢谢您先从具体的例子入手,激发问题的动机;非常有帮助! - ShreevatsaR
1
这是一个杰出的答案。它让我感到高兴的是Python只使用对象调用,让我感到难过的是我不得不使用C++。 - Claudiu
1
基本实现为 static_cast<A&&>(a) 被视为第0种情况,但在6个测试中失败了两个,详情请参见 http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2009/n2951.html。 - user2023370
显示剩余19条评论

108

我认为使用一个概念性代码实现std::forward有助于理解。这是Scott Meyers在他的演讲“An Effective C++11/14 Sampler”中的一张幻灯片(链接)

conceptual code implementing std::forward

代码中的move函数是std::move。在那个演讲之前,有一个有效的实现。我在文件move.h中找到了libstdc++中std::forward的实际实现,但它并不具有指导意义。

从用户的角度来看,它的含义是std::forward是对rvalue的条件转换。如果我写一个函数,该函数希望在参数中既接受lvalue又接受rvalue,并且只想在传递它作为rvalue时将其作为rvalue传递给另一个函数,则可以使用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

这段代码基于之前提到的演讲中的一个示例。第10张幻灯片,从开头算起约在15:00处。


3
你的第二个链接指向了完全不同的地方。 - Pharap
3
哇,解释得真好。我是从这个视频开始的:https://www.youtube.com/watch?v=srdwFMZY3Hg,但在阅读了你的回答后,终于感觉到了。 :) - flamingo

50

在完美转发中,使用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,因为它们是整数字面值,并且作为右值。但由于表达式中的t1和t2是左值,所以会调用#1而不是#2。这就是为什么我们需要使用std::forward将引用转换回未命名的引用。因此,在outer中,t1始终是左值表达式,而forward(t1)根据T1可能是右值表达式。后者只有在T1是左值引用时才是左值表达式。如果outer的第一个参数是左值表达式,则T1被推断为左值引用。

3
这是一种简化的解释,但非常好而且功能强大。人们应该先阅读这个答案,然后再深入了解。 - NicoBerrogorry
@sellibitze 再问一个问题,当推导 int a;f(a) 时,哪个语句是正确的:“由于 a 是左值,所以 int(T&&) 等同于 int(int& &&)” 还是 “为了使 T&& 等同于 int&,T 应该是 int&”?我更喜欢后者。 - John

13
如果我们将t1和t2保留为左值,那会如何影响被调用的函数inner呢?
如果,在实例化后,T1的类型是char,而T2是一个类,您想要通过复制传递t1,并通过const引用传递t2。好吧,除非inner()采用它们作为非const引用,在这种情况下,您也想要这样做。
尝试编写一组outer()函数,以没有rvalue引用的方式实现此目标,并从inner()的类型推断出正确的参数传递方式。我认为你将需要大量的模板元编程来推断参数,并花费很多时间才能为所有情况得出正确的结果。
然后,有人使用指针对参数进行inner(),这将使2^2变为3^2(或4^2。该死的,我懒得去思考const指针是否会有所不同)。
然后,想象一下您要为五个参数或七个参数执行此操作。
现在您知道为什么一些聪明的人提出了“完美转发”:它可以让编译器为您完成所有这些工作。

7
一个还没有被明确表达的点是,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)',那么这不起作用。

好的观点,所以在 static_cast 中的 T&& 也遵循引用折叠规则,对吗? - barney

5

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

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

我认为,在引入r-value引用类型后,移动和前进是自然产生的设计模式。我们不应该假设一个方法被正确使用,除非禁止不正确的使用。


我认为C++委员会并不觉得他们有责任正确使用语言习惯,甚至没有定义“正确”使用是什么(虽然他们可以提供指导方针)。为此,虽然一个人的老师、老板和朋友可能有引导他们朝某个方向发展的责任,但我认为C++委员会(因此标准)并没有这样的责任。 - SirGuy
是的,我刚读了N2951,我同意标准委员会没有义务在函数使用方面添加不必要的限制。但是这两个函数模板(move和forward)的名称确实有点令人困惑,只看库文件或标准文档中的定义(23.2.5 Forward/move helpers)。标准中的示例确实有助于理解概念,但可能有必要添加更多注释以使事情更加清晰明了。 - colin

1

从另一个角度来看,在处理通用引用赋值中的rvalues时,保留变量的类型可能是可取的。例如:

auto&& x = 2; // x is int&&
    
auto&& y = x; // But y is int&    
    
auto&& z = std::forward<decltype(x)>(x); // z is int&&

使用 std::forward,我们确保 zx 具有完全相同的类型。

此外,std::forward 不会影响左值引用:

int i;

auto&& x = i; // x is int&

auto&& y = x; // y is int&

auto&& z = std::forward<decltype(x)>(x); // z is int&

依然有zx相同的类型。

因此,回到您的情况,如果内部函数有两个重载用于int&int&&,您希望像赋值z一样传递变量而不是y

示例中的类型可以通过以下方式进行评估:

std::cout<<is_same_v<int&,decltype(z)>;
std::cout<<is_same_v<int&&,decltype(z)>;


2
假设x是一个引用,那么std::forward<decltype(x)>(x)可以简写为decltype(x)(x) - HolyBlackCat
@HolyBlackCat,说得好。我只是为了讨论而保留了std::forward - Sorush

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接