移動賦值運算子
移動賦值運算子是一個非樣板的非靜態成員函數,名稱為 operator=,它可以用相同類別類型的參數呼叫,並複製該參數的內容,同時可能修改該參數。
目錄 |
[編輯] 語法
關於正式的移動賦值運算子語法,請參閱函數宣告。下方的語法列表僅展示了所有有效移動賦值運算子語法的一個子集。
return-type operator=(parameter-list ); |
(1) | ||||||||
return-type operator=(parameter-list ) function-body |
(2) | ||||||||
return-type operator=(parameter-list-no-default ) = default; |
(3) | ||||||||
return-type operator=(parameter-list ) = delete; |
(4) | ||||||||
return-type class-name ::operator=(parameter-list ) function-body |
(5) | ||||||||
return-type class-name ::operator=(parameter-list-no-default ) = default; |
(6) | ||||||||
| class-name | - | 正在宣告移動賦值運算子的類別,在下方的說明中該類別類型以 T 表示 |
| 參數列表 | - | 僅有一個參數的參數列表,其類型為 T&&、const T&&、volatile T&& 或 const volatile T&& |
| parameter-list-no-default | - | 僅有一個參數的參數列表,其類型為 T&&、const T&&、volatile T&& 或 const volatile T&&,且不具有預設引數 |
| function-body | - | 移動賦值運算子的函數體 |
| 回傳類型 | - | 任何類型,但為了與純量類型保持一致,通常偏好使用 T& |
[編輯] 解釋
struct X { X& operator=(X&& other); // move assignment operator // X operator=(const X other); // Error: incorrect parameter type }; union Y { // move assignment operators can have syntaxes not listed above, // as long as they follow the general function declaration syntax // and do not viloate the restrictions listed above auto operator=(Y&& other) -> Y&; // OK: trailing return type Y& operator=(this Y&& self, Y& other); // OK: explicit object parameter // Y& operator=(Y&&, int num = 1); // Error: has other non-object parameters };
當移動賦值運算子被重載解析選中時就會被呼叫,例如:當一個物件出現在賦值表達式的左側,而右側是相同類型或可隱含轉換類型的右值 (rvalue) 時。
移動賦值運算子通常會轉移參數所持有的資源(例如指向動態分配物件的指標、檔案描述符、TCP 插槽、執行緒控制代碼等),而不是複製它們,並使參數處於某種有效但未指定的狀態。由於移動賦值不會改變參數的生命週期,因此參數的解構子通常會在稍後被呼叫。例如,從 std::string 或 std::vector 進行移動賦值,可能會導致參數變為空。移動賦值的定義比普通賦值更寬鬆,而不是更嚴格;普通賦值在完成時必須保留兩份數據複本,而移動賦值則只需保留一份。
[編輯] 隱含宣告的移動賦值運算子
如果類別類型沒有提供使用者定義的移動賦值運算子,且以下所有條件皆成立:
那麼編譯器將宣告一個移動賦值運算子作為其類別的 inline public 成員,簽名為 T& T::operator=(T&&)。
一個類別可以有多個移動賦值運算子,例如 T& T::operator=(const T&&) 和 T& T::operator=(T&&) 同時存在。如果已經有一些使用者定義的移動賦值運算子,使用者仍然可以使用關鍵字 default 強制生成隱含宣告的移動賦值運算子。
隱含宣告的移動賦值運算子具有異常規範,如 動態異常規範(直到 C++17) noexcept 規範(自 C++17 起) 中所述。
因為任何類別總會宣告某種賦值運算子(移動或複製),所以基類型的賦值運算子總是會被隱藏。如果使用 using-宣告來引入基類型的賦值運算子,且其參數類型可能與衍生類別的隱含賦值運算子參數類型相同,則該 using-宣告也會被隱含宣告所隱藏。
[編輯] 隱含定義的移動賦值運算子
如果隱含宣告的移動賦值運算子既未被刪除也非顯而易見,那麼當它被 odr-使用 或在常數求值中需要時(自 C++14 起),編譯器就會定義它(即生成函數體並編譯)。
對於 union 類型,隱含定義的移動賦值運算子會複製物件表現(如透過 std::memmove)。
對於非 union 的類別類型,移動賦值運算子會按照宣告順序,對物件的直接基類別和直接非靜態成員執行完整的成員級移動賦值,對標量使用內建賦值,對陣列使用成員級移動賦值,對類別類型使用其移動賦值運算子(以非虛擬方式呼叫)。
|
類別
|
(C++14 起) (至 C++23 止) |
|
類別 |
(自 C++23 起) |
與複製賦值一樣,對於透過多條路徑可到達的虛擬基類別子物件,隱含定義的移動賦值運算子是否會對其進行多次賦值是未指定的。
struct V { V& operator=(V&& other) { // this may be called once or twice // if called twice, 'other' is the just-moved-from V subobject return *this; } }; struct A : virtual V {}; // operator= calls V::operator= struct B : virtual V {}; // operator= calls V::operator= struct C : B, A {}; // operator= calls B::operator=, then A::operator= // but they may only call V::operator= once int main() { C c1, c2; c2 = std::move(c1); }
[編輯] 棄用的移動賦值運算子
如果滿足以下任一條件,類別 T 的隱含宣告或預設移動賦值運算子將被定義為棄用 (deleted):
-
T擁有 const 限定的非類別類型(或其多維陣列)的非靜態數據成員。 -
T擁有引用類型的非靜態數據成員。 -
T擁有類別類型M(或其多維陣列)的潛在構造子物件,使得在尋找M的移動賦值運算子時的重載解析:
- 沒有產生可用的候選函式,或
- 在該子物件為變體成員 (variant member) 的情況下,選擇了非平凡函式。
被刪除的隱含宣告移動賦值運算子會被重載解析忽略。
[編輯] 顯而易見的移動賦值運算子
類別 T 的移動賦值運算子在滿足以下所有條件時是顯而易見的 (trivial):
- 它不是由使用者提供的(即,它是隱含定義或預設的);
-
T沒有虛擬成員函式; -
T沒有虛擬基類; - 為
T的每個直接基類型選中的移動賦值運算子都是顯而易見的; - 為
T的每個非靜態類別類型(或類別類型陣列)成員選中的移動賦值運算子都是顯而易見的。
顯而易見的移動賦值運算子執行與顯而易見的複製賦值運算子相同的動作,即像 std::memmove 那樣複製物件表現。所有與 C 語言相容的數據類型都是顯而易見可移動賦值的。
[編輯] 合格的移動賦值運算子
|
如果移動賦值運算子未被刪除,則它是合格的 (eligible)。 |
(直到 C++20) |
|
如果滿足以下所有條件,移動賦值運算子就是合格的:
|
(自 C++20 起) |
合格移動賦值運算子的顯而易見性決定了類別是否為顯而易見可複製類型。
[編輯] 筆記
如果同時提供了複製和移動賦值運算子,當參數為右值 (rvalue)(無論是像無名臨時物件這樣的 prvalue,或是像 std::move 的結果這樣的 xvalue)時,重載解析會選擇移動賦值;當參數為左值 (lvalue)(具名物件或回傳左值引用的函數/運算子)時,會選擇複製賦值。如果僅提供了複製賦值,則所有參數類別都會選擇它(只要它以值或以 const 引用形式接收參數,因為右值可以繫結到 const 引用),這使得當移動不可用時,複製賦值成為移動賦值的備案。
對於透過多條路徑可到達的虛擬基類別子物件,隱含定義的移動賦值運算子是否會對其進行多次賦值是未指定的(這也同樣適用於複製賦值)。
請參閱賦值運算子重載以瞭解有關使用者定義移動賦值運算子預期行為的更多細節。
[編輯] 範例
#include <iostream> #include <string> #include <utility> struct A { std::string s; A() : s("test") {} A(const A& o) : s(o.s) { std::cout << "move failed!\n"; } A(A&& o) : s(std::move(o.s)) {} A& operator=(const A& other) { s = other.s; std::cout << "copy assigned\n"; return *this; } A& operator=(A&& other) { s = std::move(other.s); std::cout << "move assigned\n"; return *this; } }; A f(A a) { return a; } struct B : A { std::string s2; int n; // implicit move assignment operator B& B::operator=(B&&) // calls A's move assignment operator // calls s2's move assignment operator // and makes a bitwise copy of n }; struct C : B { ~C() {} // destructor prevents implicit move assignment }; struct D : B { D() {} ~D() {} // destructor would prevent implicit move assignment D& operator=(D&&) = default; // force a move assignment anyway }; int main() { A a1, a2; std::cout << "Trying to move-assign A from rvalue temporary\n"; a1 = f(A()); // move-assignment from rvalue temporary std::cout << "Trying to move-assign A from xvalue\n"; a2 = std::move(a1); // move-assignment from xvalue std::cout << "\nTrying to move-assign B\n"; B b1, b2; std::cout << "Before move, b1.s = \"" << b1.s << "\"\n"; b2 = std::move(b1); // calls implicit move assignment std::cout << "After move, b1.s = \"" << b1.s << "\"\n"; std::cout << "\nTrying to move-assign C\n"; C c1, c2; c2 = std::move(c1); // calls the copy assignment operator std::cout << "\nTrying to move-assign D\n"; D d1, d2; d2 = std::move(d1); }
輸出
Trying to move-assign A from rvalue temporary move assigned Trying to move-assign A from xvalue move assigned Trying to move-assign B Before move, b1.s = "test" move assigned After move, b1.s = "" Trying to move-assign C copy assigned Trying to move-assign D move assigned
[編輯] 缺陷報告
下列更改行為的缺陷報告追溯應用於之前的 C++ 標準。
| DR | 應用於 | 出版時的行為 | 正確的行為 |
|---|---|---|---|
| CWG 1353 | C++11 | 預設移動賦值運算子被 定義為刪除的條件未考慮多維陣列類型 |
考量這些型別 |
| CWG 1402 | C++11 | 一個會呼叫非顯而易見複製賦值運算子的 預設移動賦值運算子曾被 棄用;而一個被棄用的 預設移動賦值運算子仍會參與重載解析 |
允許呼叫此類 複製賦值 運算子;改為在重載解析中 被忽略 |
| CWG 1806 | C++11 | 缺少涉及虛擬基類別的預設移動賦值 運算子的規範 |
已新增 |
| CWG 2094 | C++11 | volatile 子物件曾使得預設移動賦值 運算子變為非顯而易見 (CWG issue 496) |
平凡性不受影響 |
| CWG 2180 | C++11 | 如果類別 T 是抽象的且具有非移動賦值的直接虛擬基類別, T 的預設移動賦值運算子未被定義為棄用 |
在這種情況下將運算子 定義為棄用 |
| CWG 2595 | C++20 | 如果存在另一個移動賦值運算子更受 約束但未滿足其相關約束, 則該移動賦值運算子原本是不合格的 |
在此情況下它可以是合格的 |
| CWG 2690 | C++11 | union 類型的隱含定義移動賦值 運算子未複製物件表現 |
讓它們複製 物件表現 |