-
拷貝和移動建構式定義了當用同型別的另一個物件初始化本物件時做什么,拷貝和移動賦值運算子定義了將一個物件賦予同型別的另一個物件時做什么,解構式定義了當此型別物件銷毀時做什么,我們稱這些操作為拷貝控制操作,
-
如果一個建構式的第一個引數是自身型別別的參考,且任何額外引數都有默認值,則此建構式是拷貝建構式,
-
拷貝建構式的第一個引數必須是一個參考型別,雖然我們可以定義一個接受非
const參考的拷貝建構式,但此引數幾乎總是一個const的參考, -
拷貝建構式在幾種情況下都會被隱式地使用,因此,拷貝建構式通常不應該是explicit的,
-
如果我們沒有為一個類定義拷貝建構式,編譯器會為我們定義一個,與合成默認建構式不同,即使我們定義了其他建構式,編譯器也會為我們合成一個拷貝建構式,
-
一般情況,合成的拷貝建構式會將其引數的成員逐個拷貝到正在創建的物件中,編譯器從給定物件中依次將每個非static成員拷貝到正在創建的物件中,而對于某些類來說,合成拷貝建構式用來阻止我們拷貝該型別別的物件,
-
每個成員的型別決定了它如何拷貝:對型別別的成員,會使用其拷貝建構式來拷貝;內置型別的成員則直接拷貝,雖然我們不能直接拷貝一個陣列,但合成拷貝建構式會逐元素地拷貝一個陣列型別的成員,如果陣列元素是型別別,則使用元素的拷貝建構式來進行拷貝,
-
當使用直接初始化時,我們實際上是要求編譯器使用普通的函式匹配來選擇與我們提供的引數最匹配的建構式,當我們使用拷貝初始化時,我們要求編譯器將右側運算物件拷貝到正在創建的物件中,如果需要的話還要進行型別轉換,
-
拷貝初始化通常使用拷貝建構式來完成,但是,如果一個類有一個移動建構式,則拷貝初始化有時會使用移動建構式而非拷貝建構式來完成,
-
拷貝建構式在以下幾種情況下會被使用(發生拷貝初始化):
-
拷貝初始化(用=定義變數)
-
將一個物件作為實參傳遞給一個非參考型別的形參
-
從一個回傳型別為非參考型別的函式回傳一個物件
-
用花括號串列初始化一個陣列中的元素或一個聚合類中的成員
-
某些型別別還會對它們所分配的物件使用拷貝初始化,(初始化標準庫容器或呼叫其insert/push操作時,容器會對其元素進行拷貝初始化)
-
-
拷貝建構式被用來初始化非參考型別別引數,這一特性解釋了為什么拷貝建構式自己的引數必須是參考型別,
-
如果我們希望使用一個explicit建構式,就必須顯式地使用,
vector<int> v1(10); // 正確:直接初始化 vector<int> v2 = 10; // 錯誤:接受大小引數的建構式是explicit的 void f(vector<int>); // f的引數進行拷貝初始化 f(10); // 錯誤:不能用一個explicit的建構式拷貝一個實參 f(vector<int>(10)); // 正確:從一個int直接構造一個臨時vector -
在拷貝初始化程序中,編譯器可以(但不是必須)跳過拷貝/移動建構式,直接創建物件,即,編譯器被允許將下面的代碼改寫:
string null_book = "9-999-9999-9"; // 拷貝初始化 string null_book("9-999-99999-9"); // 編譯器略過了拷貝建構式但是,即使編譯器略過了拷貝/移動建構式,但在這個程式點上,拷貝/移動建構式必須是存在且可訪問的(例如,不能是private的),
-
多載運算子的引數表示運算子的運算物件,某些運算子,包括賦值運算子,必須定義為成員函式,如果一個運算子是一個成員函式,其左側運算物件就系結到隱式的this引數,對于一個二元運算子,例如賦值運算子,其右側運算物件作為顯式引數傳遞,
-
拷貝賦值運算子接受一個與其所在類相同型別的引數:
class Foo { public: Foo& operator=(const Foo&); // 賦值運算子 // ... }; -
賦值運算子通常應該回傳一個指向其左側運算物件的參考,
-
與處理拷貝建構式一樣,如果一個類未定義自己的拷貝賦值運算子,編譯器會為它生成一個合成拷貝賦值運算子,
-
類似拷貝建構式,對于某些類,合成拷貝賦值運算子用來禁止該型別物件的賦值,如果拷貝賦值運算子并非出于此目的,它會將右側運算物件的每個非static成員賦予左側運算物件的對應成員,這一作業是通過成員型別的拷貝賦值運算子來完成的,對于陣列型別的成員,逐個賦值陣列元素,合成拷貝賦值運算子回傳一個指向其左側運算物件的參考,
// 等價于合成拷貝賦值運算子 Sales_data& Sales_data::operator=(const Sales_data &rhs) { bookNo = rhs.bookNo; // 呼叫string::operator= units_sold = rhs.units_sold; // 使用內置的int賦值 revenue = rhs.revenue; // 使用內置的double賦值 return *this; // 回傳一個此物件的參考 } -
解構式釋放物件使用的資源,并銷毀物件的非static資料成員,解構式是類的一個成員函式,名字由波浪號接類名構成,它沒有回傳值,也不接受引數:
class Foo { public: ~Foo(); // 解構式 // ... }; -
由于解構式不接受引數,因此它不能被多載,對一個給定類,只會有唯一一個解構式,
-
解構式有一個函式體和一個析構部分,在一個解構式中,首先執行函式體,然后銷毀成員,成員按初始化順序的逆序銷毀,
-
在一個解構式中,析構部分是隱式的,成員銷毀時發生什么完全依賴于成員的型別,銷毀型別別的成員需要執行成員自己的解構式,內置型別沒有解構式,因此銷毀內置型別成員什么也不需要做,
-
隱式銷毀一個內置指標型別的成員不會delete它所指向的物件,
-
無論何時一個物件被銷毀,就會自動呼叫其解構式:
- 變數在離開其作用域時被銷毀
- 當一個物件被銷毀時,其成員被銷毀
- 容器(無論是標準庫容器還是陣列)被銷毀時,其元素被銷毀
- 對于動態分配的物件,當對指向它的指標應用delete運算子時被銷毀
- 對于臨時物件,當創建它的完整運算式結束時被銷毀
-
當指向一個物件的參考或指標離開作用域時,解構式不會執行,
-
類似拷貝建構式和拷貝賦值運算子,對于某些類,合成解構式被用來阻止該型別的物件被銷毀,如果不是這種情況,合成解構式的函式體就為空,
// 下面的代碼片段等價于Sales_data的合成解構式 class Sales_data { public: // 成員會被自動銷毀,除此之外不需要做其他事情 ~Sales_data() { } // 其他成員的定義,如前 }; -
解構式體自身并不直接銷毀成員,成員是在解構式體之后隱含的析構階段中被銷毀的,在整個物件銷毀程序中,解構式體是作為成員銷毀步驟之外的另一部分而進行的,
-
當我們決定一個類是否要定義它自己版本的拷貝控制成員時,一個基本原則是首先確定這個類是否需要一個解構式,通常,對解構式的需求要比對拷貝建構式或賦值運算子的需求更為明顯,如果一個類需要自定義解構式,幾乎可以肯定它也需要自定義拷貝賦值運算子和拷貝建構式,
-
決定一個類是否要定義它自己版本的拷貝控制成員時,第二個基本原則:如果一個類需要一個拷貝建構式,幾乎可以肯定它也需要一個拷貝賦值運算子,反之亦然——如果一個類需要一個拷貝賦值運算子,幾乎可以肯定它也需要一個拷貝建構式,然而,無論是需要拷貝建構式還是需要拷貝賦值運算子都不必然意味著也需要解構式,
-
我們可以通過將拷貝控制成員定義為
=default來顯式地要求編譯器生成合成的版本, -
當我們在類內用
=default修飾成員的宣告時,合成的函式將隱式地宣告為行內的(就像任何其他類內宣告的成員函式一樣),如果我們不希望合成的成員是行內函式,應該只對成員的類外定義使用=default,class Sales_data { public: // 拷貝控制成員;使用default Sales_data() = default; Sales_data(const Sales_data&) = default; Sales_data& operator=(const Sales_data &); ~Sales_data() = default; // 其他成員的定義,如前 }; Sales_data& Sales_data::operator=(const Sales_data&) = default; -
我們只能對具有合成版本的成員函式使用
=default(即,默認建構式或拷貝控制成員), -
我們可以通過將拷貝建構式和拷貝賦值運算子定義為洗掉的函式來阻止拷貝:我們雖然宣告了它們,但不能以任何方式使用它們,在函式的引數串列后面加上
=delete來指出我們希望將它定義為洗掉的:struct NoCopy { NoCopy() = default; // 使用合成的默認建構式 NoCopy(const NoCopy&) = delete; // 阻止拷貝 NoCopy &operator=(const NoCopy&) = delete; // 阻止賦值 ~NoCopy() = default; // 使用合成的解構式 // 其他成員 }; -
與
=default不同,=delete必須出現在函式第一次宣告的時候,這個差異與這些宣告的含義在邏輯上是吻合的,一個默認的成員只影響為這個成員而生成的代碼,因此=default直到編譯器生成代碼時才需要,而另一方面,編譯器需要知道一個函式是洗掉的,以便禁止試圖使用它的操作, -
與
=default的另一個不同之處是,我們可以對任何函式指定=delete(我們只能對編譯器可以合成的默認建構式或拷貝控制成員使用=default),雖然洗掉函式的主要用途是禁止拷貝控制成員,但當我們希望引導函式匹配程序時,洗掉函式有時也是有用的, -
對于一個洗掉了解構式的型別,編譯器將不允許定義該型別的變數或創建該類的臨時物件,而且,如果一個類有某個成員的型別洗掉了解構式,我們也不能定義該類的變數或臨時物件,
-
對于洗掉了解構式的型別,雖然我們不能定義這種型別的變數或成員,但可以動態分配這種型別的物件,但是,不能釋放這些物件:
struct NoDtor { NoDtor() = default; // 使用合成默認建構式 ~NoDtor() = delete; // 我們不能銷毀NoDtor型別的物件 }; NoDtor nd; // 錯誤:NoDtor的解構式是洗掉的 NoDtor *p = new NoDtor(); // 正確:但我們不能delete p delete p; // 錯誤:NoDtor的解構式是洗掉的 -
對于解構式已洗掉的型別,不能定義該型別的變數或釋放指向該型別動態分配物件的指標,
-
對某些類來說,編譯器將這些合成的成員定義為洗掉的函式:
- 如果類的某個成員的解構式是洗掉的或不可訪問的(例如,是private的),則類的合成解構式被定義為洗掉的,
- 如果類的某個成員的拷貝建構式是洗掉的或不可訪問的,則類的合成拷貝建構式被定義為洗掉的,如果類的某個成員的解構式是洗掉的或不可訪問的,則類合成的拷貝建構式也被定義為洗掉的,
- 如果類的某個成員的拷貝賦值運算子是洗掉的或不可訪問的,或是類有一個
const的或參考成員,則類的合成拷貝賦值運算子被定義為洗掉的, - 如果類的某個成員的解構式是洗掉的或不可訪問的,或是類有一個參考成員,它沒有類內初始化器,或是類有一個
const成員,它沒有類內初始化器且其型別未顯式定義默認建構式,則該類的默認建構式被定義為洗掉的,
-
本質上,這些規則的含義是:如果一個類有資料成員不能默認構造、拷貝、賦值或銷毀,則對應的成員函式將被定義為洗掉的,(本質上,當不可能拷貝、賦值或銷毀類的成員時,類的合成拷貝控制成員就被定義為洗掉的,)
-
因為試圖訪問一個未定義的成員會導致一個鏈接時錯誤,通過宣告(但不定義)
private的拷貝建構式,我們可以預先阻止任何拷貝該型別物件的企圖:試圖拷貝物件的用戶代碼將在編譯階段被標記為錯誤;成員函式或友元函式中的拷貝操作將會導致鏈接時錯誤, -
希望阻止拷貝的類應該使用
=delete來定義它們自己的拷貝建構式和拷貝賦值運算子,而不應該將它們宣告為private的, -
通常,管理類外資源的類必須定義拷貝控制成員,為了定義這些成員,我們首先必須確定此型別物件的拷貝語意,一般來說,有兩種選擇:可以定義拷貝操作,使類的行為看起來像一個值或者像一個指標,
- 類的行為像一個值,意味著它應該也有自己的狀態,當我們拷貝一個像值的物件時,副本和原物件是完全獨立的,改變副本不會對原物件有任何影響,反之亦然,
- 行為像指標的類則共享狀態,當我們拷貝一個這種類的物件時,副本和原物件使用相同的底層資料,改變副本也會改變原物件,反之亦然,
-
關鍵概念:賦值運算子
當你撰寫賦值運算子時,有兩點需要記住:
- 如果將一個物件賦予它自身,賦值運算子必須能正確作業,
- 大多數賦值運算子組合了解構式和拷貝建構式的作業,
當你撰寫一個賦值運算子時,一個好的模式是先將右側運算物件拷貝到一個區域臨時物件中,當拷貝完成后,銷毀左側運算物件的現有成員就是安全的了,一旦左側運算物件的資源被銷毀,就只剩下將資料從臨時物件拷貝到左側運算物件的成員中了,
HasPtr& HasPtr::operator=(const HasPtr &rhs) { auto newp = new string(*rhs.ps); // 拷貝底層string delete ps; // 釋放舊記憶體 ps = newp; // 從右側運算物件拷貝資料到本物件 i = rhs.i; return *this; // 回傳本物件 }這樣撰寫賦值運算子是錯誤的
HasPtr& HasPtr::operator=(const HasPtr &rhs) { delete ps; // 釋放物件指向的string // 如果rhs和*this是同一個物件,我們就將從已釋放的記憶體中拷貝資料! ps = new string(*(rhs.ps)); i = rhs.i; return *this; } -
令一個類展現類似指標的行為的最好方法是使用
shared_ptr來管理類中的資源,但是,有時我們希望直接管理資源,在這種情況下,使用參考計數就很有用了,參考計數的作業方式如下:- 除了初始化物件外,每個建構式(拷貝建構式除外)還要創建一個參考計數,用來記錄有多少物件與正在創建的物件共享狀態,當我們創建一個物件時,只有一個物件共享狀態,因此將計數器初始化為1,
- 拷貝建構式不分配新的計數器,而是拷貝給定物件的資料成員,包括計數器,拷貝建構式遞增共享的計數器,指出給定物件的狀態又被一個新用戶所共享,
- 解構式遞減計數器,指出共享狀態的用戶少了一個,如果計數器變為0,則解構式釋放狀態,
- 拷貝賦值運算子遞增右側運算物件的計數器,遞減左側運算物件的計數器,如果左側運算物件的計數器變為0,意味著它的共享狀態沒有用戶了,拷貝賦值運算子就必須銷毀狀態,
-
實作共享計數器的一種方法是將計數器保存在動態記憶體中,
class HasPtr { public: // 建構式分配新的string和新的計數器,將計數器置為1 HasPtr(const string &s = string()) : ps(new string(s)), i(0), use(new size_t(1)) {} // 拷貝建構式拷貝所有三個資料成員,并遞增計數器 HasPtr(const HasPtr &p) : ps(p.ps), i(p.i), use(p.use) { ++*use; } HasPtr &operator=(const HasPtr &); ~HasPtr(); private: string *ps; int i; size_t *use; // 用來記錄有多少個物件共享*ps的成員 }; HasPtr &HasPtr::operator=(const HasPtr &rhs) { ++*rhs.use; // 遞增右側運算物件的參考計數 if (--*use == 0) // 然后遞減本物件的參考計數 { delete ps; // 如果沒有其他用戶 delete use; // 釋放本物件分配的成員 } ps = rhs.ps; // 將資料從rhs拷貝到本物件 i = rhs.i; use = rhs.use; return *this; // 回傳本物件 } HasPtr::~HasPtr() { if (--*use == 0) // 如果參考計數變為0 { delete ps; // 釋放string記憶體 delete use; // 釋放計數器記憶體 } } -
除了定義拷貝控制成員,管理資源的類通常還定義一個名為swap的函式,對于那些與重排元素順序的演算法一起使用的類,定義swap是非常重要的,這類演算法在需要交換兩個元素時會呼叫swap,
-
如果一個類定義了自己的swap,那么演算法將使用類自定義版本,否則,演算法將使用標準庫定義的swap,
class HasPtr { friend void swap(HasPtr&, HasPtr&); // 其他成員定義 }; // 由于swap的存在就是為了優化代碼,我們將其宣告為inline函式, inline void swap(HasPtr &lhs, HasPtr &rhs) { using std::swap; swap(lhs.ps, rhs.ps); // 交換指標,而不是string資料 swap(lhs.i, rhs.i); // 交換int成員 } -
與拷貝控制成員不同,swap并不是必要的,但是,對于分配了資源的類,定義swap可能是一種很重要的優化手段,
-
swap函式應該呼叫swap,而不是std::swap,內置型別是沒有特定版本的swap的,對swap的呼叫會呼叫標準庫
std::swap,但是,如果一個類的成員有自己型別特定的swap函式,呼叫std::swap就是錯誤的了, -
如果存在型別特定的swap版本,swap呼叫會與之匹配,如果不存在型別特定的版本,則會使用
std中的版本(假定作用域中有using宣告), -
定義swap的類通常用swap來定義它們的賦值運算子,這些運算子使用了一種名為拷貝并交換的技術,這種技術將左側運算物件與右側運算物件的一個副本進行交換:
// 注意rhs是按值傳遞的,意味著HasPtr的拷貝建構式... // ...將右側運算物件中的string拷貝到rhs HasPtr& HasPtr::operator=(HasPtr rhs) { // 交換左側運算物件和區域變數rhs的內容 swap(*this, rhs); // rhs現在指向本物件曾經使用的記憶體 return *this; // rhs被銷毀,從而delete了rhs中的指標 } -
使用拷貝并交換的賦值運算子自動就是例外安全的,且能正確處理自賦值,
-
雖然通常來說分配資源的類更需要拷貝控制,但資源管理并不是一個類需要定義自己的拷貝控制成員的唯一原因,一些類也需要拷貝控制成員的幫助來進行簿記作業或其他操作,
-
拷貝賦值運算子通常執行拷貝建構式和解構式中也要做的作業,這種情況下,公共的作業應該放在private的工具函式中完成,
-
在某些情況下,物件拷貝后就立即被銷毀了,在這些情況下,移動而非拷貝物件會大幅度提升性能,
-
標準庫容器、
string和shared_ptr類既支持移動也支持拷貝,IO類和unique_ptr類可以移動但不能拷貝, -
右值參考——即必須系結到右值的參考,我們通過&&而不是&來獲得右值參考,右值參考只能系結到一個將要銷毀的物件,因此,我們可以自由地將一個右值參考的資源“移動”到另一個物件中,
-
對于常規參考(我們可以稱之為左值參考),我們不能將其系結到要求轉換的運算式、字面常量或是回傳右值的運算式,右值參考可以系結到這類運算式上,但不能將一個右值參考直接系結到一個左值上
int i = 42; int &r = i; // 正確:r參考i int &&rr = i; // 錯誤:不能將一個右值參考系結到一個左值上 int &r2 = i * 42; // 錯誤:i*42是一個右值 const int &r3 = i * 42; // 正確:我們可以將一個const的參考系結到一個右值上 int &&rr2 = i * 42; // 正確:將rr2系結到乘法結果上 -
回傳左值參考的函式,連同賦值、下標、解參考和前置遞增/遞減運算子,都是回傳左值的運算式的例子,可以將一個左值參考系結到這類運算式的結果上,回傳非參考型別的函式,連同算術、關系、位以及后置遞增/遞減運算子,都生成右值,可以將一個
const的左值參考或者一個右值參考系結到這類運算式上, -
左值持久,右值短暫:左值有持久的狀態,而右值要么是字面常量,要么是在運算式求值程序中創建的臨時物件,
-
由于右值參考只能系結到臨時物件,我們得知:
- 所參考的物件將要被銷毀
- 該物件沒有其他用戶
這兩個特性意味著:使用右值參考的代碼可以自由地接管所參考的物件的資源,
-
右值參考指向將要被銷毀的物件,因此,我們可以從系結到右值參考的物件“竊取”狀態,
-
變數可以看作只有一個運算物件而沒有運算子的運算式,變數運算式都是左值,我們不能將一個右值參考直接系結到一個變數上,即使這個變數是右值參考型別也不行,
int &&rr1 = 42; // 正確:字面常量是右值 int &&rr2 = rr1; // 錯誤:運算式rr1是左值! -
通過呼叫一個名為
move的標準庫函式來獲得系結到左值上的右值參考,此函式定義在頭檔案utility中,int &&rr3 = std::move(rr1); // ok -
我們可以銷毀一個移后源物件,也可以賦予它新值,但不能使用一個移后源物件的值,
-
使用move的代碼應該使用
std::move而不是move,這樣做可以避免潛在的名字沖突, -
除了完成資源移動,移動建構式還必須確保移后源物件處于這樣一個狀態——銷毀它是無害的,特別是,一旦資源完成移動,源物件必須不再指向被移動的資源——這些資源的所有權已經歸屬新創建的物件,
StrVec::StrVec(StrVec &&s) noexcept // 移動操作不應拋出任何例外 // 成員初始化器接管s中的資源 : elements(s.elements), first_free(s.first_free), cap(s.cap) { // 令s進入這樣的狀態——對其運行解構式是安全的 s.elements = s.first_free = s.cap = nullptr; } -
與拷貝建構式不同,移動建構式不分配任何新記憶體;它接管給定的物件中的記憶體,在接管記憶體之后,它將給定物件中的指標都置為
nullptr,這樣就完成了從給定物件的移動操作,此物件將繼續存在,最終,移后源物件會被銷毀,意味著將在其上運行解構式, -
由于移動操作“竊取”資源,它通常不分配任何資源,因此,移動操作通常不會拋出任何例外,當撰寫一個不拋出例外的移動操作時,我們應該將此事通知標準庫,除非標準庫知道我們的移動建構式不會拋出例外,否則它會認為移動我們的類物件時可能會拋出例外,并且為了處理這種可能性而做一些額外的作業,
-
一種通知標準庫的方法是在我們的建構式中指明
noexcept,在一個建構式中,noexcept出現在引數串列和初始化串列開始的冒號之間:class StrVec { public: StrVec(StrVec&&) noexcept; // 移動建構式 // 其他成員的定義,如前 }; StrVec::StrVec(StrVec &&s) noexcept : /* 成員初始化器 */ { /* 建構式體 */ }我們必須在類頭檔案的宣告中和定義中(如果定義在類外的話)都指定
noexcept, -
不拋出例外的移動建構式和移動賦值運算子必須標記為
noexcept, -
標記
noexcept的原因:首先,雖然移動操作通常不拋出例外,但拋出例外也是允許的;其次,標準庫容器能對例外發生時其自身的行為提供保障,例如,vector保證,如果我們呼叫push_back時發生例外,vector自身不會發生改變,除非vector知道元素型別的移動建構式不會拋出例外,否則在重新分配記憶體的程序中,它就必須使用拷貝建構式(如果拷貝發生例外,vector可以釋放新分配的記憶體并回傳,vector原有的元素仍然存在)而不是移動建構式(如果移動發生例外,舊空間中的移動源元素已經被改變了,而新空間中未構造的元素可能尚不存在),如果希望在vector重新分配記憶體這類情況下對我們自定義型別的物件進行移動而不是拷貝,就必須顯式地告訴標準庫我們的移動建構式可以安全使用,我們通過將移動建構式(及移動賦值運算子)標記為noexcept來做到這一點:StrVec &StrVec::operator=(StrVec &&rhs) noexcept { // 直接檢測自賦值 if (this != &rhs) { free(); // 釋放已有元素 elements = rhs.elements; // 從rhs接管資源 first_free = rhs.first_free; cap = rhs.cap; // 將rhs置于可析構狀態 rhs.elements = rhs.first_free = rhs.cap = nullptr; } return *this; } -
在移動操作之后,移后源物件必須保持有效(一般來說,物件有效就是指可以安全地為其賦予新值或者可以安全地使用而不依賴其當前值)的、可析構的狀態,但是用戶不能對其值進行任何假設,
-
如果一個類定義了自己的拷貝建構式、拷貝賦值運算子或者解構式,編譯器就不會為它合成移動建構式和移動賦值運算子了,如果一個類沒有移動操作,通過正常的函式匹配,類會使用對應的拷貝操作來代替移動操作,
-
只有當一個類沒有定義任何自己版本的拷貝控制成員,且它的所有(非static)資料成員都能移動構造或移動賦值時,編譯器才會為它合成移動建構式或移動賦值運算子,
-
與拷貝操作不同,移動操作永遠不會隱式定義為洗掉的函式,但是,如果我們顯式地要求編譯器生成
=default的移動操作,且編譯器不能移動所有成員,則編譯器會將移動操作定義為洗掉的函式, -
除了一個重要例外,什么時候將合成的移動操作定義為洗掉的函式遵循與定義洗掉的合成拷貝操作類似的原則:
- 與拷貝建構式不同,移動建構式被定義為洗掉的函式的條件是:有類成員定義了自己的拷貝建構式且未定義移動建構式,或者是有類成員未定義自己的拷貝建構式且編譯器不能為其合成移動建構式,移動賦值運算子的情況類似,
- 如果有類成員的移動建構式或移動賦值運算子被定義為洗掉的或是不可訪問的,則類的移動建構式或移動賦值運算子被定義為洗掉的,
- 類似拷貝建構式,如果類的解構式被定義為洗掉的或不可訪問的,則類的移動建構式被定義為洗掉的,
- 類似拷貝賦值運算子,如果有類成員是
const的或是參考,則類的移動賦值運算子被定義為洗掉的,
-
如果類定義了一個移動建構式和/或一個移動賦值運算子,則該類的合成拷貝建構式和拷貝賦值運算子會被定義為洗掉的,因此,定義了一個移動建構式或移動賦值運算子的類必須也定義自己的拷貝操作,否則,這些成員默認地被定義為洗掉的,
-
如果一個類既有移動建構式,也有拷貝建構式,編譯器使用普通的函式匹配規則來確定使用哪個建構式,賦值操作的情況類似,
StrVec v1, v2; v1 = v2; // v2是左值;使用拷貝賦值 StrVec getVec(istream &); // getVec回傳一個右值 v2 = getVec(cin); // getVec(cin)是一個右值;使用移動賦值 -
如果一個類沒有移動建構式,函式匹配規則保證該型別的物件會被拷貝,即使我們試圖通過呼叫move來移動它們時也是如此:
class Foo { public: Foo() = default; Foo(const Foo&); // 拷貝建構式 // 其他成員定義,但Foo未定義移動建構式 }; Foo x; Foo y(x); // 拷貝建構式;x是一個左值 Foo z(std::move(x)); // 拷貝建構式,因為未定義移動建構式總結為:移動右值,拷貝左值,但如果沒有移動建構式,右值也被拷貝,拷貝賦值運算子和移動賦值運算子的情況類似
-
拷貝并交換賦值運算子和移動操作:依賴于實參的型別,
rhs的拷貝初始化要么使用拷貝建構式,要么使用移動建構式——左值被拷貝,右值被移動,因此,單一的賦值運算子就實作了拷貝賦值運算子和移動賦值運算子兩種功能:class HasPtr { public: // 添加的移動建構式 HasPtr(HasPtr &&p) noexcept : ps(p.ps), i(p.i) { p.ps = 0; } // 賦值運算子既是移動賦值運算子,也是拷貝賦值運算子 HasPtr& operator=(HasPtr rhs) { swap(*this, rhs); return *this; } // 其他成員的定義 }; -
更新三/五法則:一般來說,如果一個類定義了任何一個拷貝操作,它就應該定義所有五個操作,
-
一般來說,拷貝一個資源會導致一些額外開銷,在這種拷貝并非必要的情況下,定義了移動建構式和移動賦值運算子的類就可以避免此問題,
-
移動建構式:從無到有,不需要判斷自賦值;移動賦值運算子:從有到有,需要檢查自賦值,
-
移動迭代器配接器:一般來說,一個迭代器的解參考運算子回傳一個指向元素的左值,而移動迭代器的解參考運算子生成一個右值參考,
-
make_move_iterator函式將一個普通迭代器轉換為一個移動迭代器,此函式接受一個迭代器引數,回傳一個移動迭代器,原迭代器的所有其他操作在移動迭代器中都照常作業,由于移動迭代器支持正常的迭代器操作,我們可以將一對移動迭代器傳遞給演算法,特別是,可以將移動迭代器傳遞給
uninitialized_copy:void StrVec::reallocate() { // 分配大小兩倍于當前規模的記憶體空間 auto newcapacity = size() ? 2 * size() : 1; auto first = alloc.allocate(newcapacity); // 移動元素 auto last = uninitialized_copy(make_move_iterator(begin()), make_move_iterator(end()), first); free(); // 釋放舊空間 elements = first; // 更新指標 first_free = last; cap = elements + newcapacity; }uninitialized_copy對輸入序列中的每個元素呼叫construct來將元素“拷貝”到目的位置,此演算法使用迭代器的解參考運算子從輸入序列中提取元素,由于我們傳遞給它的是移動迭代器,因此解參考運算子生成的是一個右值參考,這意味著construct將使用移動建構式來構造元素, -
由于移動一個物件可能銷毀掉原物件,因此你只有在確信演算法在為一個元素賦值或將其傳遞給一個用戶定義的函式后不再訪問它時,才能將移動迭代器傳遞給演算法,
-
由于一個移后源物件具有不確定的狀態,對其呼叫
std::move是危險的,當我們呼叫move時,必須絕對確認移后源物件沒有其他用戶,通過在類代碼中小心地使用move,可以大幅度提升性能,而如果隨意在普通用戶代碼(與類實作代碼相對)中使用移動操作,很可能導致莫名其妙的、難以查找的錯誤,而難以提升應用程式性能,在移動建構式和移動賦值運算子這些類實作代碼之外的地方,只有當你確信需要進行移動操作且移動操作是安全的,才可以使用std::move, -
允許移動的成員函式通常使用與拷貝/移動建構式和賦值運算子相同的引數模式——一個版本接受一個指向
const的左值參考,第二個版本接受一個指向非const的右值參考,// 假定X是元素型別 void push_back(const X&); // 拷貝:系結到任意型別的X void push_back(X&&); // 移動:只能系結到型別X的可修改的右值 -
一般來說,我們不需要為函式操作定義接受一個
const X&&或是一個(普通的)X&引數的版本,當我們希望從實參“竊取”資料時,通常傳遞一個右值參考,為了達到這一目的,實參不能是const的,類似的,從一個物件進行拷貝的操作不應該改變該物件,因此,通常不需要定義一個接受一個(普通的)X&引數的版本, -
區分移動和拷貝的多載函式通常有一個版本接受一個
const T&,而另一個版本接受一個T&&, -
通常,我們在一個物件上呼叫成員函式,而不管該物件是一個左值還是一個右值,
string s1 = "a value", s2 = "another"; auto n = (s1 + s2).find('a'); s1 + s2 = "wow!"; -
在引數串列后放置一個參考限定符(&或&&)來指出this的左值/右值屬性,類似
const限定符,參考限定符只能用于(非static)成員函式,且必須同時出現在函式的宣告和定義中,class Foo { public: Foo &operator=(const Foo&) &; // 只能向可修改的左值賦值 // Foo的其他引數 }; Foo &Foo::operator=(const Foo &rhs) & { // 執行將rhs賦予本物件所需的作業 return *this; } -
對于&限定的函式,我們只能將它用于左值;對于&&限定的函式,只能用于右值,
Foo &retFoo(); // 回傳一個參考;retFoo呼叫是一個左值 Foo retVal(); // 回傳一個值;retVal呼叫是一個右值 Foo i, j; // i和j是左值 i = j; // 正確:i是左值 retFoo() = j; // 正確:retFoo()回傳一個左值 retVal() = j; // 錯誤:retVal()回傳一個右值 i = retVal(); // 正確:我們可以將一個右值作為賦值操作的右側運算物件 -
一個函式可以同時用
const和參考限定,在此情況下,參考限定符必須跟隨在const限定符之后:class Foo { public: Foo someMem() & const; // 錯誤:const限定符必須在前 Foo anotherMem() const &; // 正確:const限定符在前 }; -
參考限定符也可以區分多載版本,而且,我們可以綜合參考限定符和
const來區分一個成員函式的多載版本,class Foo { public: Foo sorted() &&; // 可用于可改變的右值 Foo sorted() const &; // 可用于任何型別的Foo // Foo的其他成員的定義 private: vector<int> data; }; // 本物件為右值,因此可以原址排序 Foo Foo::sorted() && { sort(data.begin(), data.end()); return *this; } // 本物件是const或是一個左值,哪種情況我們都不能對其進行原址排序 Foo Foo::sorted() const & { Foo ret(*this); // 拷貝一個副本 sort(ret.data.begin(), ret.data.end()); // 排序副本 return ret; // 回傳副本 }編譯器會根據呼叫sorted的物件的左值/右值屬性來確定使用哪個sorted版本:
retVal().sorted(); // retVal()是一個右值,呼叫Foo::sorted() && retFoo().sorted(); // retFoo()是一個左值,呼叫Foo::sorted() const & -
如果一個成員函式有參考限定符,則具有相同引數串列的所有版本都必須有參考限定符,
class Foo { public: Foo sorted() &&; Foo sorted() const; // 錯誤:必須加上參考限定符 // Comp是函式型別的型別別名 // 此函式型別可以用來比較int值 using Comp = bool(const int&, const int&); Foo sorted(Comp*); // 正確:不同的引數串列 Foo sorted(Comp*) const; // 正確:兩個版本都沒有參考限定符 };
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/260486.html
標籤:其他
