在谈论右值引用时,区分引用生命周期中两个不相关的关键步骤是很重要的——绑定和值语义。
这里的绑定是指调用函数时值与参数类型匹配的确切方式。
例如,如果您有函数重载:
void foo(int a) {}
void foo(int&& a) {}
然后在调用时foo(x)
,选择适当的重载的行为涉及将值绑定x
到 的参数foo
。
右值引用仅与绑定语义有关。
在两个 foo 函数的主体内,该变量a
充当常规左值。也就是说,如果我们这样重写第二个函数:
void foo(int&& a) {
foo(a);
}
那么直觉上这应该会导致堆栈溢出。但事实并非如此——右值引用都是关于绑定的,而不是关于值语义的。由于a
是函数体内的常规左值,因此foo(int)
将在该点调用第一个重载并且不会发生堆栈溢出。只有当我们显式更改 的值类型时才会发生堆栈溢出a
,例如通过使用std::move
:
void foo(int&& a) {
foo(std::move(a));
}
此时,由于值语义发生了变化,将发生堆栈溢出。
在我看来,这是右值引用最令人困惑的特性——类型在绑定期间和之后的工作方式不同。绑定时它是一个右值引用,但之后它就像一个左值引用。在所有方面,右值引用类型的变量在绑定完成后就像左值引用类型的变量一样。
左值和右值引用之间的唯一区别在于绑定时 - 如果同时存在左值和右值重载可用,则临时对象(或者更确切地说是 xvalues - eXpiring 值)将优先绑定到右值引用:
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()
它本身实际上并没有做任何事情 - 特别是它没有做任何动作。它只是将其论点标记为“准备好被淘汰”。
最后一点是:右值引用仅在与左值引用一起使用时才有用。没有右值参数本身有用的情况(这里夸大了)。
假设您有一个接受字符串的函数:
void foo(std::string);
如果函数只是检查字符串而不是复制它,那么使用const&
:
void foo(const std::string&);
这总是在调用函数时避免复制。
如果函数要修改或存储字符串的副本,则使用按值传递:
void foo(std::string s);
在这种情况下,如果调用者传递一个左值,您将收到一份副本,并且临时对象将在原地构造,避免复制。std::move(s)
如果您想存储 s 的值,则使用,例如在成员变量中。请注意,即使调用者传递了一个右值引用,这也会有效地工作,foo(std::move(mystring));
因为std::string
它提供了一个移动构造函数。
在这里使用右值是一个糟糕的选择:
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&);
在这种情况下使用右值引用在技术上是可行的,但在语义上不正确且完全令人困惑。
右值引用唯一有意义的地方是移动构造函数或移动赋值运算符。在任何其他情况下,按值传递或左值引用通常是正确的选择,可以避免很多混乱。
注意:不要将右值引用与看起来完全相同但工作方式完全不同的转发引用混淆,如下所示:
template <class T>
void foo(T&& t) {
}
在上面的例子中t
看起来像一个右值引用参数,但实际上是一个转发引用(因为模板类型),这是一个完全不同的蠕虫罐。