std::forward在传递左值/右值引用时是如何工作的,特别是如何工作的?

172
可能重复:
std::forward的主要目的是什么,它解决了哪些问题? 我知道它的作用和使用时机,但我仍然无法理解它的工作原理。请尽可能详细地解释,如果允许使用模板参数推导,什么情况下使用std::forward会不正确。
我困惑的部分是: "如果有名字,它就是左值" - 如果是这样的话,为什么当我传递thing&& x和thing& x时,std::forward的行为会不同呢?

14
在这里回答:std::forward实际上只是对static_cast<T&&>的一种语法糖。 - Nicol Bolas
2
简短的回答是为什么你不能让类型被推断:在“template <typename T> void foo(T && x);”的主体中,x的类型与被推断为T的类型不同。 - Kerrek SB
似乎缺少的概念是,类型(例如int)与“值类别”不是同一件事情(如果您使用变量int a,则int有时可以是lvalue,如果您从函数int fun()返回它,则有时是rvalue)。当您查看参数thing&& x时,它的类型是一个rvalue reference,但是,名为x的变量也具有值类别:它是一个lvaluestd :: forward <>将确保转换“值类别”x以匹配其类型。它确保将thing&x作为值类别lvalue传递,并将thing && x作为rvalue传递。 - arkan
说 std::forward 等同于 static_cast<T&&> 是令人困惑的,因为我们混淆了类型和值类别这两个不同的概念。 - arkan
在34:06 Scott Meyers解释了类型和值是两个不同的概念(标题:“C++和Beyond 2012:Scott Meyers - C++11中的通用引用”) - arkan
3个回答

270
我认为将std::forward解释为static_cast<T&&>是令人困惑的。我们对于强制转换的直觉是它将一种类型转换为其他类型 - 在这种情况下,它将转换为右值引用。但实际上并不是这样!因此,我们使用另一种神秘的方法来解释一个神秘的东西。这种特殊的转换由Xeo的回答中的表定义。但问题是:为什么?所以这是我的理解:
假设我想要传递给你一个std::vector<T> v,你应该将其作为数据成员_v存储在你的数据结构中。天真(且安全)的解决方案是始终将向量复制到其最终目标中。因此,如果您通过中介函数(方法)执行此操作,则该函数应声明为采用引用。 (如果您将其声明为按值采用矢量,则会执行额外的完全不必要的副本。)
void set(const std::vector<T> & v) { _v = v; }

如果你手头有一个左值,那么这一切都很好,但是如果你手头有一个右值呢?假设这个向量是调用函数makeAndFillVector()的结果。如果你执行了直接赋值:

_v = makeAndFillVector();

编译器会移动向量而不是复制它。但如果引入中介set(),则有关参数rvalue属性的信息将丢失并进行复制。
set(makeAndFillVector()); // set will still make a copy

为了避免这种复制,你需要使用"完美转发",这将每次产生最优代码。如果给出一个左值,你希望你的函数将其视为左值并进行复制。如果给出一个右值,你希望你的函数将其视为右值并移动它。
通常情况下,你可以通过分别对左值和右值重载函数set()来实现。
set(const std::vector<T> & lv) { _v = v; }
set(std::vector<T> && rv) { _v = std::move(rv); }

但现在想象一下,您正在编写一个模板函数,该函数接受T并使用该T调用set()(不用担心我们的set()仅针对向量定义的事实)。诀窍在于,当使用左值实例化模板函数时,您希望此模板调用set()的第一个版本,并在使用右值初始化时调用第二个版本。
首先,这个函数的签名应该是什么?答案是:
template<class T>
void perfectSet(T && t);

根据您调用此模板函数的方式,类型 T 的推断结果会有所不同。如果您使用左值进行调用:

std::vector<T> v;
perfectSet(v);

向量v将会通过引用传递。但如果你使用右值调用它:

perfectSet(makeAndFillVector());

(匿名)向量将通过右值引用传递。因此,C++11魔法被有意设置为尽可能保留参数的rvalue特性。

现在,在perfectSet内部,您想要完美地将参数传递给set()的正确重载。这就是需要使用std::forward的地方:

template<class T>
void perfectSet(T && t) {
    set(std::forward<T>(t));
}

如果没有使用std::forward,编译器会假设我们想通过引用传递t。为了让你自己相信这一点,可以将以下代码进行比较:

void perfectSet(T && t) {
    set(t);
    set(t); // t still unchanged
}

转换为:

void perfectSet(T && t) {
    set(std::forward<T>(t));
    set(t); // t is now empty
}

如果您没有显式地转发t,编译器必须防御性地假设您可能会再次访问t,并选择set的左值引用版本。但是,如果您转发t,则编译器将保留其rvalue状态,并调用rvalue引用版本的set()。此版本将移动t的内容,这意味着原始内容变为空。

这个答案比我最初想象的要长得多 ;-)


6
void set(**const** std::vector & v) { _v = v; } 可以翻译为:void set(**const** std::vector & v) { _v = v; }。不要过度复杂化问题。 - Howard Hinnant
5
在这种情况下,它将转换为右值引用。但事实并非如此!在您的“perfectSet”中,变量“t”已经是左值。通过使用“static_cast”(或“std::forward”),我们将其转回成右值。 - Xeo
2
@Xeo:除非您使用向量的引用调用perfectSet。例如:vector v; vector & vr; perfectSet(vr);当您将左值引用转换为右值引用时,结果仍然是左值引用。这就是我所说的。 - Bartosz Milewski
Bartosz,为什么不将set()作为参数来获取前向引用,而不是使用两个重载函数? - RaGa__M
谢谢。到目前为止,这是最好的解释。 - Pavlo
显示剩余6条评论

203
首先,让我们看看根据标准,std::forward 的作用是什么:

§20.2.3 [forward] p2

返回值: static_cast<T&&>(t)

(其中T是明确指定的模板参数,t是传递的参数。)

现在回想一下引用折叠规则:

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)

(从这个答案无耻地抄袭。)

接下来,让我们看一个想要使用完美转发的类:

template<class T>
struct some_struct{
  T _v;
  template<class U>
  some_struct(U&& v)
    : _v(static_cast<U&&>(v)) {} // perfect forwarding here
                                 // std::forward is just syntactic sugar for this
};

现在来看一个调用的例子:

int main(){
  some_struct<int> s1(5);
  // in ctor: '5' is rvalue (int&&), so 'U' is deduced as 'int', giving 'int&&'
  // ctor after deduction: 'some_struct(int&& v)' ('U' == 'int')
  // with rvalue reference 'v' bound to rvalue '5'
  // now we 'static_cast' 'v' to 'U&&', giving 'static_cast<int&&>(v)'
  // this just turns 'v' back into an rvalue
  // (named rvalue references, 'v' in this case, are lvalues)
  // huzzah, we forwarded an rvalue to the constructor of '_v'!

  // attention, real magic happens here
  int i = 5;
  some_struct<int> s2(i);
  // in ctor: 'i' is an lvalue ('int&'), so 'U' is deduced as 'int&', giving 'int& &&'
  // applying the reference collapsing rules yields 'int&' (& + && -> &)
  // ctor after deduction and collapsing: 'some_struct(int& v)' ('U' == 'int&')
  // with lvalue reference 'v' bound to lvalue 'i'
  // now we 'static_cast' 'v' to 'U&&', giving 'static_cast<int& &&>(v)'
  // after collapsing rules: 'static_cast<int&>(v)'
  // this is a no-op, 'v' is already 'int&'
  // huzzah, we forwarded an lvalue to the constructor of '_v'!
}

希望这个逐步解答能够帮助您和其他人更好地理解std::forward的工作原理。


73
(从这个回答中无耻地偷来。)不要多想,他们是从这里偷来的:http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2002/n1377.htm#References%20to%20References :-) - Howard Hinnant
5
我真正困惑的是为什么不能在std::forward中使用模板参数推导,但我不想用这样的话来问,因为之前我已经试过一次,但没有理解结果。不过我现在想我已经弄清楚了。(链接:https://dev59.com/OWsz5IYBdhLWcg3wmpL0#8862379) - David
2
我想我永远不会知道,但是...为什么要踩我呢?我错过了什么吗?我说错了什么吗? - Xeo
另一个“不要脸”的来源:https://msdn.microsoft.com/zh-cn/library/dd293668.aspx:* - Isilmë O.
2
我会送给你一篮鲜花和一大块巧克力。谢谢! - NicoBerrogorry
显示剩余12条评论

0

它能够工作是因为在调用完美转发时,类型T不是值类型,还可以是引用类型。

例如:

template<typename T> void f(T&&);
int main() {
    std::string s;
    f(s); // T is std::string&
    const std::string s2;
    f(s2); // T is a const std::string&
}

因此,forward 可以简单地查看显式类型 T,以查看您实际传递给它的内容。当然,如果我记得正确,这样做的确切实现是非常复杂的,但那就是信息所在之处。

当您引用一个命名的右值引用时,那确实是一个左值。然而,forward 通过上述方式检测到它实际上是一个右值,并正确返回要转发的右值。


啊哈!你能否为std::string &s, std::string&& s, const std::string&& s, std::string* s和std::string* const s添加更多例子(以及T是什么)? - David
1
@Dave:不是的,其实有很多教程更深入地介绍了引用折叠。 - Puppy

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