了解右值参考

时间:2012-09-26 16:47:12

标签: c++ c++11 rvalue-reference

我认为我对rvalue引用并不十分了解。为什么以下错误编译(VS2012)时出现错误'foo' : cannot convert parameter 1 from 'int' to 'int &&'

void foo(int &&) {}
void bar(int &&x) { foo(x); };

我会假设从bar传递到foo时会保留类型int &&。为什么它会在函数体内部转换为int

我知道答案是使用std::forward

void bar(int &&x) { foo(std::forward<int>(x)); }

所以也许我只是没有清楚地掌握为什么。 (另外,为什么不std::move?)

5 个答案:

答案 0 :(得分:11)

  

为什么它会在函数体内转换为int

没有;它仍然是对 rvalue 的引用。

当某个名称出现在表达式中时,它就是左值 - 即使它恰好是对 rvalue 的引用。如果表达式要求(即如果需要它的值),它可以转换为 rvalue ;但它不能绑定到 rvalue 引用。

正如您所说,为了将其绑定到另一个 rvalue 引用,您必须将其显式转换为未命名的 rvalue std::forwardstd::move是方便的方法。

  

另外,为什么不std::move

为什么不呢?这比std::forward更有意义,{{1}}适用于不知道参数是否为参考的模板。

答案 1 :(得分:10)

我总是记得左值是一个有名字或可以被解决的值。由于x具有名称,因此它将作为左值传递。引用rvalue的目的是允许函数以它认为合适的任何方式完全破坏值。如果我们像你的例子那样通过引用传递x,那么我们无法知道这样做是否安全:

void foo(int &&) {}
void bar(int &&x) { 
    foo(x); 
    x.DoSomething();   // what could x be?
};

执行foo(std::move(x));明确告诉编译器您已完成x并且不再需要它。如果没有这一举动,现有代码就会发生不好的事情。 std::move是一种保护措施。

std::forward用于模板中的完美转发。

答案 2 :(得分:9)

这是“no name rule”。在bar内,x的名称为x。所以它现在是一个左值。将某些东西作为右值引用传递给函数并不会使它成为函数内的右值。

如果您不明白为什么必须这样,请问问自己 - x返回后foo是什么? (请记住,foo可以自由移动x。)

答案 3 :(得分:1)

rvalue lvalue 表达式的类别。

右值参考左值参考参考的类别。

在声明T x&& = <initializer expression>中,变量x作为T&amp;&amp;类型,并且它可以绑定到表达式(the),它是 rvalue表达式。因此,T&amp;&amp;已被命名为 rvalue引用类型,因为它引用了 rvalue表达式

在声明T x& = <initializer expression>中,变量x作为T&amp;类型,它可以绑定到一个表达式(),它是一个左值表达式(++)。因此,T&amp;已被命名为左值引用类型,因为它可以引用左值表达式

在C ++中,重要的是在声明中出现的实体命名与表达式中出现此名称之间做出区分。

当表达式出现在foo(x)中的表达式中时,名称x本身就是一个表达式,称为id-expression。根据定义,id-expression始终是左值表达式左值表达式不能绑定到右值引用

答案 4 :(得分:0)

在谈论右值引用时,重要的是区分引用生命周期中两个关键的不相关步骤-绑定和值语义。

这里的绑定是指调用函数时将值与参数类型匹配的确切方式。

例如,如果您有函数重载:

void foo(int a) {}
void foo(int&& a) {}

然后,在调用foo(x)时,选择适当的重载的操作涉及将值x绑定到foo的参数上。

右值引用仅与绑定语义有关。

在两个foo函数的主体内部,变量a充当常规左值。也就是说,如果我们这样重写第二个函数:

void foo(int&& a) {
    foo(a);
} 

然后直观地讲,这将导致堆栈溢出。但这不是-rvalue引用全都与绑定有关,而与值语义无关。由于a是函数体内的常规左值,因此将在该点调用第一个重载foo(int),并且不会发生堆栈溢出。仅当我们显式更改a的值类型时,才会发生堆栈溢出。通过使用std::move

void foo(int&& a) {
    foo(std::move(a));
}

此时,由于值语义的更改,将发生堆栈溢出。

在我看来,这是右值引用最令人困惑的功能-在绑定期间和绑定之后,该类型的工作方式有所不同。绑定时是右值引用,但此后它像左值引用一样。 在所有方面,绑定完成后,右值引用类型的变量的行为就像左值引用类型的变量。

绑定时,左值和右值引用之间的唯一区别是-如果同时存在左值和右值重载,则将优先将临时对象(或xvalues-扩展值)绑定到右值引用:

void goo(const int& x) {}
void goo(int&& x) {}

goo(5); // this will call goo(int&&) because 5 is an xvalue

那是唯一的区别。从技术上讲,除了约定外,没有什么阻止您使用右值引用(例如左值引用):

void doit(int&& x) {
    x = 123;
}

int a;
doit(std::move(a));
std::cout << a; // totally valid, prints 123, but please don't do that

这里的关键字是“惯例”。由于右值引用优先绑定到临时对象,因此可以合理地假设您可以删除临时对象,即将其所有数据移离它,因为在调用之后,它无论如何都无法访问,并且无论如何都会被销毁:

std::vector<std::string> strings;
string.push_back(std::string("abc"));

在上面的代码段中,临时对象std::string("abc")不能在出现语句之后以任何方式使用,因为它没有绑定到任何变量。因此,push_back可以移走其内容而不是复制它,从而节省了额外的分配和取消分配。

也就是说,除非您使用std::move

std::vector<std::string> strings;
std::string mystr("abc");
string.push_back(std::move(mystr));

现在,在调用mystr之后仍可以访问对象push_back,但是push_back不知道这一点-仍假定它被允许删除该对象,因为它已被传递作为右值参考。这就是std::move()的行为是惯例之一的原因,也是std::move()本身实际上不做任何事情的原因-特别是它不做任何动作。它只是将其论据标记为“准备被摧毁”。


最后一点是:仅右值引用与左值引用一起使用时才有用。在任何情况下,rvalue参数本身都不有用(在此夸大)。

假设您有一个接受字符串的函数:

void foo(std::string);

如果函数仅检查字符串而不是复制字符串,则使用const&

void foo(const std::string&);

这在调用函数时始终避免复制。

如果函数要修改或存储字符串的副本,请使用传递值:

void foo(std::string s);

在这种情况下,如果调用方传递一个左值,您将收到一个副本,并且临时对象将就地构造,避免复制。如果要存储s的值,请使用std::move(s),例如在成员变量中。请注意,即使调用者传递了右值引用(即foo(std::move(mystring));,因为std::string提供了move构造函数,这也将有效地工作。

在此处使用右值是一个糟糕的选择:

void foo(std::string&&)

因为它将准备对象的重担放在了调用者身上。特别是如果调用者想将字符串的副本传递给此函数,则必须显式地执行该操作;

std::string s;
foo(s); // XXX: doesn't compile
foo(std::string(s)); // have to create copy manually

如果您想将可变引用传递给变量,只需使用常规的左值引用即可:

void foo(std::string&);

在这种情况下,使用右值引用在技术上是可行的,但在语义上不适当且完全令人困惑。

在右值引用中唯一有意义的地方是在move构造函数或move赋值运算符中。在任何其他情况下,按值传递或左值引用通常是正确的选择,并且避免了很多混乱。


注意:请勿将右值引用与看起来完全相同但工作方式完全不同的转发引用混淆,例如:

template <class T>
void foo(T&& t) {
}

在上面的示例中,t看起来像是右值引用参数,但实际上是转发引用(由于模板类型),这是完全不同的蠕虫病毒。