首页 > 编程笔记

C++11右值引用详解

能出现在赋值号左边的表达式称为“左值”,不能出现在赋值号左边的表达式称为“右值”。一般来说,左值是可以取地址的,右值则不可以。

非 const 的变量都是左值。函数调用的返回值若不是引用,则该函数调用就是右值。前面所学的“引用”都是引用变量的,而变量是左值,因此它们都是“左值引用”。

C++11 新增了一种引用,可以引用右值,因而称为“右值引用”。无名的临时变量不能出现在赋值号左边,因而是右值。右值引用就可以引用无名的临时变量。定义右值引用的格式如下:

类型 && 引用名 = 右值表达式;

例如:
class A{};
A & rl = A();  //错误,无名临时变量 A() 是右值,因此不能初始化左值引用 r1
A && r2 = A();  //正确,因 r2 是右值引用

引入右值引用的主要目的是提高程序运行的效率。有些对象在复制时需要进行深复制,深复制往往非常耗时。合理使用右值引用可以避免没有必要的深复制操作。例如下面的程序:
#include <iostream>
#include <string>
#include <cstring>
using namespace std;
class String
{
public:
    char* str;
    String() : str(new char[1]) { str[0] = 0; }
    String(const char* s) {
        str = new char[strlen(s) + 1];
        strcpy(str, s);
    }
    String(const String & s) {//复制构造函数
        cout << "copy constructor called" << endl;
        str = new char[strlen(s.str) + 1];
        strcpy(str, s.str);
    }
    String & operator = (const String & s) {//复制赋值号
        cout << "copy operator = called" << endl;
        if (str != s.str) {
            delete[] str;
            str = new char[strlen(s.str) + 1];
            strcpy(str, s.str);
        }
        return *this;
    }
    String(String && s) : str(s.str) { //移动构造函数
        cout << "move constructor called" << endl;
        s.str = new char[1];
        s.str[0] = 0;
    }
    String & operator = (String && s) { //移动赋值号
        cout << "move operator = called" << endl;
        if (str != s.str) {
            str = s.str;
            s.str = new char[1];
            s.str[0] = 0;
        }
        return *this;
    }
    ~String() { delete[] str; }
};
template <class T>
void MoveSwap(T & a, T & b) {
    T tmp(move(a));  //std::move(a) 为右值,这里会调用移动构造函数
    a = move(b);  //move(b) 为右值,因此这里会调用移动赋值号
    b = move(tmp);  //move(tmp) 为右值,因此这里会调用移动赋值号
}
int main()
{
    String s;
    s = String("this");  //调用移动赋值号
    cout << "* * * *" << endl;
    cout << s.str << endl;
    String s1 = "hello", s2 = "world";
    MoveSwap(s1, s2);  //调用一次移动构造函数和两次移动赋值号
    cout << s2.str << endl;
    return 0;
}
程序的输出结果如下:
move operator = called
****
this
move constructor called
move operator = called
move operator = called
hello

第 33 行重载了一个移动赋值号。它和第 19 行的复制赋值号的区别在于,其参数是右值引用。在移动赋值号函数中没有执行深复制操作,而是直接将对象的 str 指向了参数 s 的成员变量 str 指向的地方,然后修改 s.str 让它指向别处,以免 s.str 原来指向的空间被释放两次。

该移动赋值号函数修改了参数,这会不会带来麻烦呢?答案是不会。因为移动赋值号函数的形参是一个右值引用,则调用该函数时,实参一定是右值。右值一般是无名临时变量,而无名临时变量在使用它的语句结束后就不再有用,因此其值即使被修改也没有关系。

第 53 行,如果没有定义移动赋值号,则会导致复制赋值号被调用,引发深复制操作。临时无名变量String("this")是右值,因此在定义了移动赋值号的情况下,会导致移动赋值号被调用。移动赋值号使得 s 的内容和 String("this") 一致,然而却不用执行深复制操作,因而效率比复制赋值号高。

虽然移动赋值号修改了临时变量 String("this"),但该变量在后面已无用处,因此这样的修改不会导致错误。

第 46 行使用了 C++ 11 中的标准模板 move。move 能接受一个左值作为参数,返回该左值的右值引用。因此本行会用定义于第 28 行、以右值引用作为参数的移动构造函数来初始化 tmp。该移动构造函数没有执行深复制,将 tmp 的内容变成和 a 相同,然后修改 a。由于调用 MoveSwap 本来就会修改 a,所以 a 的值在此处被修改不会产生问题。

第 47 行和第 48 行调用了移动赋值号,在没有进行深复制的情况下完成了 a 和 b 内容的互换。对比 Swap 函数的以下写法:
template <class T>
void Swap(T & a, T & b) {
    T tmp(a);  //调用复制构造函数
    a=b;  //调用复制赋值号
    b=tmp;  //调用复制赋值号
}
Swap 函数执行期间会调用一次复制构造函数,两次复制赋值号,即一共会进行三次深复制操作。而利用右值引用,使用 MoveSwap,则可以在无须进行深复制的情况下达到相同的目的,从而提高了程序的运行效率。

推荐阅读