右值引用

DyanBlog發表於2024-06-22

右值引用

右值引用是 C++11 引入的與 Lambda 表示式齊名的重要特性之一。它的引入解決了 C++ 中大量的歷史遺留問題,消除了諸如 std::vector、std::string 之類的額外開銷,也才使得函式物件容器 std::function 成為了可能。

左值、右值的純右值、將亡值、右值

要弄明白右值引用到底是怎麼一回事,必須要對左值和右值做一個明確的理解。

左值 (lvalue, left value),顧名思義就是賦值符號左邊的值。準確來說,左值是表示式(不一定是賦值表示式)後依然存在的持久物件。

右值 (rvalue, right value),右邊的值,是指表示式結束後就不再存在的臨時物件。

而 C++11 中為了引入強大的右值引用,將右值的概念進行了進一步的劃分,分為:純右值、將亡值。純右值 (prvalue, pure rvalue)

  • 純粹的右值,要麼是純粹的字面量,例如 10, true;
  • 要麼是求值結果相當於字面量或匿名臨時物件,例如 1+2。
  • 非引用返回的臨時變數、運算表示式產生的臨時變數、原始字面量、Lambda 表示式都屬於純右值。

需要注意的是,字串字面量只有在類中才是右值,當其位於普通函式中是左值。例如:

class Foo {
    const char*&& right = "this is a rvalue"; // 此處字串字面量為右值

public:
    void bar() {
    right = "still rvalue"; // 此處字串字面量為右值
}
};

int main() {
const char* const &left = "this is an lvalue"; // 此處字串字面量為左值
}

將亡值 (xvalue, expiring value),是C++11 為了引入右值引用而提出的概念(因此在傳統 C++
中,純右值和右值是同一個概念),也就是即將被銷燬、卻能夠被移動的值。

將亡值可能稍有些難以理解,我們來看這樣的程式碼:

std::vector<int> foo() {
    std::vector<int> temp = {1, 2, 3, 4};
    return temp;
}
std::vector<int> v = foo();

在這樣的程式碼中,就傳統的理解而言,函式 foo 的返回值 temp 在內部建立然後被賦值給 v,然而 v 獲得這個物件時,會將整個 temp 複製一份,然後把 temp 銷燬,如果這個 temp 非常大,這將造成大量額外的開銷(這也就是傳統 C++ 一直被詬病的問題)。在最後一行中,v 是左值、foo() 返回的值就是右值(也是純右值)。但是,v 可以被別的變數捕獲到,而 foo() 產生的那個返回值作為一個臨時值,一旦被 v 複製後,將立即被銷燬,無法獲取、也不能修改。而將亡值就定義了這樣一種行為:臨時的值能夠被識別、同時又能夠被移動。

在 C++11 之後,編譯器為我們做了一些工作,此處的左值 temp 會被進行此隱式右值轉換,等價於static_cast<std::vector &&>(temp),進而此處的 v 會將 foo 區域性返回的值進行移動。也就是後面我們將會提到的移動語義。

右值引用和左值引用

要拿到一個將亡值,就需要用到右值引用:T &&,其中 T 是型別。右值引用的宣告讓這個臨時值的生命週期得以延長、只要變數還活著,那麼將亡值將繼續存活。

C++11 提供了 std::move 這個方法將左值引數無條件的轉換為右值,有了它我們就能夠方便的獲得一個右值臨時物件,例如:

#include <iostream>
#include <string>
39
3.3 右值引用 第 3 章語言執行期的強化
void reference(std::string& str) {
    std::cout << " 左值" << std::endl;
}
void reference(std::string&& str) {
    std::cout << " 右值" << std::endl;
}
int main()
{
    std::string lv1 = "string,"; // lv1 是一個左值
    // std::string&& r1 = lv1; // 非法, 右值引用不能引用左值
    std::string&& rv1 = std::move(lv1); // 合法, std::move 可以將左值轉移為右值
    std::cout << rv1 << std::endl; // string,
    const std::string& lv2 = lv1 + lv1; // 合法, 常量左值引用能夠延長臨時變數的生命週期
    // lv2 += "Test"; // 非法, 常量引用無法被修改
    std::cout << lv2 << std::endl; // string,string
    std::string&& rv2 = lv1 + lv2; // 合法, 右值引用延長臨時物件生命週期
    rv2 += "Test"; // 合法, 非常量引用能夠修改臨時變數
    std::cout << rv2 << std::endl; // string,string,string,Test
    reference(rv2); // 輸出左值
    return 0;
}

rv2 雖然引用了一個右值,但由於它是一個引用,所以 rv2 依然是一個左值。注意,這裡有一個很有趣的歷史遺留問題,我們先看下面的程式碼:

#include <iostream>
int main() {
    // int &a = std::move(1); // 不合法,非常量左引用無法引用右值
    const int &b = std::move(1); // 合法, 常量左引用允許引用右值
    std::cout << a << b << std::endl;
}

第一個問題,為什麼不允許非常量引用繫結到非左值?這是因為這種做法存在邏輯錯誤:

void increase(int & v) {
    v++;
}
void foo() {
    double s = 1;
    increase(s);
}

由於 int& 不能引用 double 型別的引數,因此必須產生一個臨時值來儲存 s 的值,從而當
increase() 修改這個臨時值時,從而呼叫完成後 s 本身並沒有被修改。

第二個問題,為什麼常量引用允許繫結到非左值?原因很簡單,因為 Fortran 需要。

相關文章