C++11右值參照詳解

2020-07-16 10:04:27
能出現在賦值號左邊的表示式稱為“左值”,不能出現在賦值號左邊的表示式稱為“右值”。一般來說,左值是可以取地址的,右值則不可以。

非 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,則可以在無須進行深複製的情況下達到相同的目的,從而提高了程式的執行效率。