C++ 值類別與移動語意

最後更新: 2021-01-25 02:14:28 +0800 +0800
原文以英文撰寫。您正在閱讀AI翻譯版本。

移動語意是多載選擇與型別定義的狀態轉移,不是自動的擁有權轉移。請先從運算式的值類別開始,再選擇能描述擁有權與生命週期的參數型別。

運算式類別 #

每個非 void 運算式恰好屬於下列其中一種類別:

  • 左值識別物件、函式或位元欄位。
  • 將亡值識別其資源可以重複使用的物件。
  • 純右值計算值或初始化物件。

左值與將亡值是廣義左值;純右值與將亡值是右值。具名變數即使型別為 T&&,仍是左值運算式。

cpp
int& counter();
int&& cache_slot();

counter() = 42;              // The call is an lvalue.
int&& slot = cache_slot();
consume(slot);               // `slot` is an lvalue expression.
consume(std::move(slot));    // Casts it to an xvalue.

回傳 T 的呼叫是純右值,回傳 T& 的呼叫是左值,回傳 T&& 的呼叫是將亡值。std::move 執行將亡值轉型;它不會移動資料,也不會結束來源物件的生命週期。

移動建構是多載選擇 #

參照繫結提供相關區別:

  • T& 繫結相容的左值。
  • const T& 繫結左值、將亡值與純右值。
  • T&& 繫結將亡值與純右值。
cpp
std::string source = "payload";
std::string destination = std::move(source); // Selects a move constructor when viable.

選取的建構函式定義轉移方式。它可能轉移配置、複製小型表示,或產生與複製無法區別的效果。依值回傳區域變數時通常應使用 return local;:複製省略可能直接建構結果,而不適用省略時,符合資格的區域變數可以在多載選擇中被視為將亡值。return std::move(local); 可能抑制具名回傳值最佳化。

轉送參照保留呼叫端的類別 #

推導樣板參數中的 T&& 是轉送參照。對左值引數,T 推導為 U&;對右值,T 推導為 U。參照摺疊會將 U& && 簡化為 U&,因此 std::forward<T> 可以重現原始類別。

cpp
template<class T>
void relay(T&& value) {
    consume(std::forward<T>(value));
}

std::move(value) 會將兩種情況都轉為將亡值,可能意外從左值呼叫端移動。未使用推導的 T&&(例如類別樣板中 T 已固定的成員)是一般右值參照,而不是轉送參照。

參數設計從擁有權開始 #

使用描述被呼叫端契約的參數形式:

形式契約
const T&在呼叫期間以唯讀方式借用。
T&在呼叫期間借用並變更。
T取得獨立值;呼叫端複製或移動至其中。
T&&要求可由被呼叫端消費的右值;不蘊含擁有權。
std::unique_ptr<T>轉移獨佔擁有權。
std::shared_ptr<T>被呼叫端保留一份擁有權時共用擁有權。
T*借用,API 明確表達可為空性。

儲存獨立值的建構函式可以依值取得參數,並從參數移動:

cpp
class Message {
public:
    explicit Message(std::string text) : text_{std::move(text)} {}
private:
    std::string text_;
};

左值呼叫端會將值複製至參數;右值呼叫端可以移動至其中。查詢或僅借用物件時,不適合依值傳遞。

不要使用 FILE*&& 或其他原始指標右值參照來表示擁有權。移動原始指標只會複製其位址;兩個名稱仍指向相同資源。呼叫會轉移銷毀責任時,請使用具擁有權的包裝器,例如 std::unique_ptr<T, Deleter>。只使用智慧指標所擁有物件的函式,應依存取權與可為空性接受 T&、const T&、T* 或 const T*;不需要接受智慧指標控制代碼。

被移動來源的物件仍然存在 #

標準函式庫物件被移動後,除非規格另有說明,會處於有效但未指定的值。銷毀、指定,以及前置條件接受該狀態的作業都是有效的。不要假設被移動的 std::vector 為空或保留容量。

cpp
std::vector<int> buffer = read_row();
rows.push_back(std::move(buffer));
buffer.clear(); // Valid, but capacity reuse is not guaranteed.

借用不得超過擁有者的生命週期。移動擁有 unique_ptr 不會銷毀指向物件,也不會銷毀所有既有借用,但之後銷毀或重設新的擁有者可能使那些借用懸空。移動作業解決表示轉移;它不會消除陳述生命週期的需求。

評論

在 GitHub 上閱讀或評論(發表評論需要登入)