1. 程式人生 > >c++11:物件移動 & 右值引用 & 移動建構函式

c++11:物件移動 & 右值引用 & 移動建構函式

一、概述

c++ 11 新標準中最主要的特徵是可以移動而非拷貝物件的能力。很多情況下,物件拷貝後就會立即被銷燬。
在這些情況下,移動而非拷貝物件會大幅度提升效能。

在舊 C++ 標準中,沒有直接的方法移動物件。因此,即使不必要拷貝物件的情況下,我們也不得不拷貝。如果物件本身要求分配記憶體,進行不必要的拷貝代價非常高。類似的,在舊版本的標準庫中,容器所儲存的型別必須是可拷貝的,但在新標準中,我們可以用容器儲存不可拷貝的型別,只要它們可以被移動即可。

移動,簡單來說是解決各種情形下物件的資源所有權轉移問題。

二、左值、右值與右值引用

2.1 左值與右值

左值與右值是從 C 語言繼承過來的概念,原本是為了幫助記憶:左值可以出現在賦值語句的左邊與右邊,而右值只能出現在賦值語句的右邊,如下面的賦值表示式:

a = b + c;

a 就是一個左值,而 b + c 則是一個右值。這種識別左值右值的方法在 C++ 中依然有效。不過 C++ 中有更廣泛認同的說法,那就是:

可以取地址的、有名字的就是左值,反之不能取地址的、沒有名字的就是右值。

分析上面的加法表示式,&a 是允許的操作,但 &(b + c) 這樣的操作則不會通過編譯。因此 a 是一個左值,(b + c) 則是右值。

這種判別方式非常有效,但是很感性,下面介紹更為細緻的 C++11 中的判定方式。

–> 區分左值、純右值與將亡值
  1. 純右值:C++ 98 標準中的右值的概念,用於辨識臨時變數與一些不跟物件關聯的值,幾種比較程式設計比較典型的純右值:
1. 非引用返回的函式返回的臨時變數值
2. 運算表示式,如 (1+2) 產生的臨時變數值
3. 不與物件相關聯的字面量值,如:2、true
4. 型別轉換函式的返回值
5. lambda 表示式
  1. 將亡值:C++11 新增的跟右值引用相關的表示式,這樣的表示式通常是將要被移動的物件(資源所有權轉移),通常是返回右值引用 T&& 的函式返回值、std::move 的返回值等。
  2. 左值:除純右值與將亡值外可以標識函式、物件的值都屬於左值。

在 C++11 程式中,所有的值必屬於左值、將亡值、純右值三者之一。

2.2 右值引用

為了支援移動語義,新標準中引入了一種必須繫結到右值得引用叫做右值引用,相比左值引用,右值引用通過 && 而不是 & 來獲取,如我們將看到的,右值引用有一個重要的性質——只能繫結到一個將要銷燬的物件上,因此我們可以很安全的將右值引用的資源”移動”到另一個物件中。

int i = 12;
int &r = i;         // 左值引用
int &&r1 = i * 12;  // 右值引用

const int &r3 = i * 10; // 我們可以將一個 conts 引用繫結到右值中
–> 常量左值引用

《const int &r3》被稱為常量引用型別,它可以接受左值、右值對其進行初始化。而且在使用右值對其進行初始化時,常量左值引用也可以像右值引用一樣將右值的生命期延長,不過相比於右值引用所引用的右值,常量左值引用所引用的右值在它接下來的宣告週期只能是隻讀的。

T &e = ReturnRValue();          // 編譯失敗
const T &f = ReturnRValue();    // 編譯通過
–> 左值持久;右值短暫

觀察左值與右值的特徵,兩者的區別很明顯:左值是持久的狀態,而右邊要麼是字面常量,要麼是臨時變數,是將要被銷燬的物件。

++這意味著:使用右值引用的程式碼可以自由安全的接管所引用物件的資源。++

2.3 std::move:強制轉化為右值

在 C++11 中,標準庫在 中提供了一個有用的函式 std::move,這個函式並不移動任何東西,它唯一的功能就是將一個左值強制轉化為右值引用,繼而可以通過右值繼續使用該值,以用於移動語義。

T &&r = std::move(lvalue);

從實現上來講,std::move 基本等價於一個型別轉換:

static_cast<T&&>(lvalue);

三、移動語義

前面引申出了右值引用,而在 C++11 的設計中,右值引用的意義有兩大作用:移動語義和完美轉發。我們主要講一下移動語義。

移動語義,簡單來說解決的是各種情形下物件的資源所有權轉移的問題。

舉個栗子。

問題:如何將大象從冰箱A轉移到另一臺冰箱B

  • 回答一:開啟冰箱A門,取出大象,關上冰箱A門,開啟另一臺冰箱B門,放進大象,關上冰箱B門。
  • 回答二:將冰箱B直接套到大象上,然後將冰箱A銷燬。

回答二是一種典型的”移動”思想,右值中的資料可以被安全移走這一特性使得右值被用來表達移動語義,以同類型的右值構造物件時,需要以引用形式傳入引數。右值引用顧名思義專門用來引用右值,左值引用和右值引用可以被分別過載,這樣確保左值和右值分別呼叫到拷貝和移動的兩種語義實現。

對於左值,如果我們明確放棄對其資源的所有權,則可以通過 std::move() 來將其轉為右值引用。std::move() 實際上是 static_cast

3.1 移動建構函式和移動賦值運算子

移動建構函式和移動賦值運算子是移動語義最典型的一個體現。

類似 string 類,如果我們自己的類也同時支援移動和拷貝,那麼也能從中受益。為了讓我們自己的型別支援移動操作,需要為其定義移動建構函式和移動賦值運算子。與拷貝建構函式不同的是,它們從給定物件竊取資源而不是拷貝資源。

移動建構函式的第一個引數是該類型別的一個右值引用。

class Foo {
public:
    Foo() : ptr(new int(0)) {
        std::cout << "<--- Construct --->" << std::endl;
    }

    // 移動建構函式 
    Foo(Foo &&foo) : ptr(foo.ptr) {
        // 令 ptr 進入這樣的狀態——對其進行解構函式時安全的
        foo.ptr = nullptr;
        std::cout << "<--- Move construct --->" << std::endl;
    }

    int *ptr;
}

與拷貝建構函式不同,移動建構函式不分配任何新記憶體,它從給定的 foo 物件中接管記憶體,並將給定 foo 物件指標都置為 nullptr,這使得對給定物件執行解構函式時安全的。

移後源物件必須可析構

值得注意的是,從一個給定物件移動資料並不會銷燬此物件,但必須保證,經過”移動”操作後,源物件是可以被安全銷燬的,也就是當編寫一個移動操作後,必須保證移後源物件進入一個可析構的狀態。

為了滿足這一要求,我們通過將移後源物件的指標置為 nullptr 來實現。

預設移動建構函式

與拷貝建構函式不同的是,只有類的每個非 static 資料都可移動時,編譯器才會為它合成移動建構函式或移動賦值表示式。編譯器可以移動內建型別的成員,如果一個成員是類型別,且該類有對應的移動操作,編譯器也能移動這個成員。

class Foo {
    int i;          // 內建型別可以移動
    std::string s;  // string 類定義了自己的移動操作
}

Foo foo;
// 使用移動建構函式
foo1 = Foo(std::move(foo));

如果一個類沒有移動操作,通過正常的函式匹配,類會使用對應的拷貝操作來代替移動。

移動右值,拷貝左值…

如果一個類既有移動建構函式,又有拷貝建構函式,編譯器使用普通的函式匹配規則來確定使用哪個建構函式。

有一個比較簡單口訣是:移動右值,拷貝左值

Foo f1,f2;
Foo f2 = f1;    // f1 是左值,使用拷貝構造
Foo getFoo();   // getFoo 函式返回一個右值
f1 = getFoo();  // getFoo 返回右值,使用移動賦值

3.2 –

auto action = std::make_unique<Action>(false, filename, line);
if (!action->InitTriggers(triggers, err)) {
    return false;
}

action_ = std::move(action);