右值引用

左值(赋值操作符“=”的左侧,通常是一个变量)与右值(赋值操作符“=”的右侧,通常是一个常数、表达式、函数调用)之间的差别可以追溯到 Christopher Strachey (C++的祖先语言CPL之父,指称语义学之父)时代。在C++中,左值可被绑定到非const引用,左值或者右值则可被绑定到const引用。但是却没有什么可以绑定到非const的右值(译注:即右值无法被非const的引用绑定),这是为了防止人们修改临时变量的值,这些临时变量在被赋予新的值之前,都会被销毁。例如:

void incr(int& a) { ++a; }
int i = 0;
incr(i);    // i变为1
//错误:0不是一个左值
incr(0);
//(译注:0不是左值,无法直接绑定到非const引用:int&。
// 假如可行,那么在调用时,将会产生一个值为0的临时变量,
// 用于绑定到int&中,但这个临时变量将在函数返回时被销毁,
// 因而,对于它的任何更改都是没有意义的,
// 所以编译器拒绝将临时变量绑定到非const引用,但对于const的引用,
// 则是可行的)

假设incr(0)合法,那么要么产生一个程序员不可见的临时变量并进行无意义的递增操作,要么发生更悲剧的情况:把字面常量“0”的实际值会变成1。尽管后者听起来是天方夜谭,但是对于早期Frotran等这一类把字面常量“0”也存到内存里的某个位置的编译器来说,这真的会变成一个bug。(译注:指的是假如把用于存储字面常量0的内存单元上的值从0修改为1以后,后续所有使用字面常数0的地方实际上都在使用“1”)。

到目前为止,一切都很美好。但考虑如下函数:

template<class T> swap(T& a, T& b) // 老式的swap函数
{
    T tmp(a);// 现在有两份"a"
    a = b;        // 现在有两份"b"
    b = tmp;    // 现在有两份tmp(值同a)
}

如果T是一个拷贝代价相当高昂的类型,例如string和vector,那么上述swap()操作也将煞费气力(不过对于标准库来说,我们已经针对string和vector的swap()进行了特化来处理这个问题)。注意这个奇怪的现象,我们的初衷其实并不是为了把这些变量拷来拷去,我是仅仅是想将变量a,b,tmp的值做一个“移动”(译注:即通过tmp来交换a,b的值)。

在C++11中,我们可以定义“移动构造函数(move constructors)”和“移动赋值操作符(move assignments”来“移动”而非复制它们的参数:

template<class T> class vector {
        // …
        vector(const vector&);  // 拷贝构造函数
        vector(vector&&);   // 移动构造函数
        vector& operator= (const vector&); // 拷贝赋值函数
        vector& operator =(vector&&);  // 移动赋值函数
}; //注意:移动构造函数和移动赋值操作符接受
// 非const的右值引用参数,而且通常会对传入的右值引用参数作修改

”&&”表示“右值引用”。右值引用可以绑定到右值(但不能绑定到左值):

X a;
X f();
X& r1 = a;        // 将r1绑定到a(一个左值)
X& r2 = f();    // 错误:f()的返回值是右值,无法绑定
X&& rr1 = f();    // OK:将rr1绑定到临时变量
X&& rr2 = a;    // 错误:不能将右值引用rr2绑定到左值a

移动赋值操作背后的思想是,“赋值”不一定要通过“拷贝”来做,还可以通过把源对象简单地“偷换”给目标对象来实现。例如对于表达式s1=s2,我们可以不从s2逐字拷贝,而是直接让s1“侵占”s2内部的数据存储(译注:比如char* p),并以某种方式“删除”s1中原有的数据存储(或者干脆把它扔给s2,因为大多情况下s2随后就会被析构)。(译注:仔细体会copy与move的区别。)

我们如何知道源对象能否“移动”呢?我们可以这样告诉编译器:(译注:通过move()操作符)

template <class T>
void swap(T& a, T& b)  //“完美swap”(大多数情况下)
{
      T tmp = move(a);  // 变量a现在失效(译注:内部数据被move到tmp中了)
      a = move(b);      // 变量b现在失效(译注:内部数据被move到a中了,变量a现在“满血复活”了)
      b = move(tmp);    // 变量tmp现在失效(译注:内部数据被move到b中了,变量b现在“满血复活”了)
}

move(x) 意味着“你可以把x当做一个右值”,把move()改名为rval()或许会更好,但是事到如今,move()已经使用很多年了。在C++11中,move()模板函数(参考“brief introduction”),以及右值引用被正式引入。

右值引用同时也可以用作完美转发(perfect forwarding)。(译注:比如某个接口函数什么也不做,只是将工作“委派”给其他工作函数)

在C++11的标准库中,所有的容器都提供了移动构造函数和移动赋值操作符,那些插入新元素的操作,如insert()和push_back(), 也都有了可以接受右值引用的版本。最终的结果是,在没有用户干预的情况下,标准容器和算法的性能都提升了,而这些都应归功于拷贝操作的减少。

参考:

(翻译:dabaitu,感谢:dave)