拷贝控制——拷贝控制和资源管理,交换操作,对象移动
一、拷貝控制和資源管理
? 通常,管理類外資源的類必須定義拷貝控制成員,這種類需要通過析構函數來釋放對象所分配的資源。
為了定義這些成員,我們首先必須確定此類型對象的拷貝語義。一般來說,有兩種選擇:可以定義拷貝操作,使類的行為看起來像一個值或者像一個指針。
類的行為像一個值,意味著它應該也有自己的狀態。當我們拷貝一個像值的對象時,副本和原對象是完全獨立的。改變副本不會對原對象有任何影響,反之亦然。
行為像指針的類則共享狀態。當我們拷貝一個這種類的對象時,副本和原對象使用相同的底層數據。改變副本也會改變原對象,反之亦然。
在我們使用的標準庫類中,標準庫容器和string類的行為像一個值,shared_ptr類提供類似指針的行為。
?
1、行為像值的類
為了提供類值的行為,對于類管理的資源,每個對象都應該擁有一份自己的拷貝。
1)類值拷貝賦值運算符
賦值運算符通常組合了析構函數和構造函數的操作。類似析構函數,賦值操作會銷毀左側運算對象的資源。類似拷貝構造函數,賦值操作會從右側運算對象拷貝數據。但是,非常重要的一點是,這些操作是以正確的順序進行的,即使將一個對象賦予它自身,也保證正確。而且,如果可能,我們編寫的賦值運算符還應該是異常安全的——當異常發生時能將左側運算對象置于一個有意義的狀態。
注意:對于一個賦值運算符來說,正確工作是非常重要的,即使是將一個對象賦予它自身,也要能正確工作。一個好的方法是在銷毀左側運算對象資源之前拷貝右側運算對象。
1 #include <iostream> 2 #include <string> 3 4 class Demo 5 { 6 public: 7 Demo(const std::string &s = std::string()) : 8 ps(new std::string(s)), data(0) {} 9 Demo(const Demo &rhs) : // 每個字符串都有自己的拷貝 10 ps(new std::string(*rhs.ps)), data(rhs.data) {} 11 Demo &operator=(const Demo &rhs) 12 { 13 auto newp = new std::string(*rhs.ps); // 拷貝底層string 14 delete ps; // 釋放舊內存 15 ps = newp; // 從右側運算對象拷貝數據到本對象 16 data = rhs.data; 17 return *this; 18 } 19 ~Demo() { 20 delete ps; 21 } 22 std::string *ps; 23 int data; 24 }; 25 26 int main() 27 { 28 Demo a, b; 29 *b.ps = "bb"; 30 a = b; 31 std::cout << *a.ps << " " << *b.ps << std::endl; 32 *b.ps = "cc"; 33 std::cout << *a.ps << " " << *b.ps << std::endl; 34 return 0; 35 } View Code?在本例中,通過先拷貝右側運算對象,我們可以處理自賦值情況,并能保證在異常發生時代碼也是安全的。
2、行為像指針的類
對于行為類似指針的類,我們需要為其定義拷貝構造函數和拷貝賦值運算符,來拷貝指針成員本身而不是它指向的string。我們的類仍需要自己的析構函數來釋放接受string參數的構造函數分配的內存。但是,在本例中,析構函數不能單方面地釋放關聯的string。只有當最后一個指向string的Demo銷毀時,它才可以釋放string。
令一個類展現類似指針的行為的最好的辦法是使用shared_ptr來管理類中的資源??截惢蛸x值一個shared_ptr會拷貝(賦值)shared_ptr所指向的指針。shared_ptr類自己記錄有多少用戶共享它所指向的對象。當沒有用戶使用對象時,shared_ptr類負責釋放資源。
但是,有時我們希望直接管理資源。在這種情況下,使用引用計數就很有用了。為了說明引用計數如何工作,我們將重新定義Demo,令其行為像指針一樣,但我們不使用shared_ptr,而是設計自己的引用計數。
1)引用計數
? 引用計數的工作方式如下:
- 除了初始化對象外,每個構造函數(拷貝構造函數除外)還要創建一個引用計數,用來記錄有多少對象與正在創建的對象共享狀態。當我們創建一個對象時,只有一個對象共享狀態,因此將計數器初始化為1。
- 拷貝構造函數不分配新的計數器,而是拷貝給定對象的數據成員,包括計數器。拷貝構造函數遞增共享的計數器,指出給定對象的狀態又被一個新用戶所共享。
- 析構函數遞減計數器,指出共享狀態的用戶少了一個。如果計數器變為0,則析構函數釋放狀態。
- 拷貝賦值運算符遞增右側運算對象的計數器,遞減左側運算對象的計數器。如果左側運算對象的計數器變為0,意味著它的共享狀態沒有用戶了,拷貝賦值運算符就必須銷毀狀態。
計數器要保存在動態內存中。當創建一個對象時,我們分配一個新的計數器。當拷貝或賦值對象時,我們拷貝指向計數器的指針。使用這種方法,副本和原對象都會指向相同的計數器。
1 #include <iostream> 2 #include <string> 3 #include <memory> 4 5 class Demo 6 { 7 public: 8 Demo(const std::string &s = std::string()): 9 ps(new std::string(s)), data(0), use(new std::size_t(1)){} 10 11 Demo(const Demo &rhs): // 12 ps(new std::string(*rhs.ps)), data(rhs.data), use(rhs.use){ 13 ++*use; 14 } 15 16 Demo &operator=(const Demo &rhs) 17 { 18 ++*rhs.use; // 先遞增右側運算對象的計數器 19 if (--*use==0) // 遞減本對象的計數器 20 { 21 std::cout << "operator= delete " << *ps << std::endl; 22 delete ps; 23 delete use; 24 } 25 ps = rhs.ps; 26 data = rhs.data; 27 use = rhs.use; 28 return *this; 29 } 30 31 ~Demo() 32 { 33 if (--*use == 0) 34 { 35 std::cout << "~Demo " << *ps << std::endl; 36 delete ps; 37 delete use; 38 } 39 } 40 std::string *ps; 41 int data; 42 std::size_t *use; // 計數器 43 }; 44 45 int main() 46 { 47 Demo a("hi"), b("hello"); 48 a = b; 49 std::cout << "-----------" << std::endl; 50 return 0; 51 } View Code?
二、交換操作
除了定義拷貝控制成員,管理資源的類通常還定義一個名為swap的函數。對于那些與重排元素順序的算法一起使用的類,定義swap是非常重要的。這類算法在交換兩個元素時會調用swap。
如果一個類定義來了自己的swap,那么算法將使用類自定義版本。否則,算法將使用標準庫定義的swap。
1)自定義swap函數
1 #include <iostream> 2 #include <string> 3 #include <memory> 4 5 class Demo 6 { 7 friend void swap(Demo &, Demo &); 8 public: 9 Demo(const std::string &s = std::string()) : 10 ps(new std::string(s)), data(0){} 11 Demo(const Demo &rhs) : // 每個字符串都有自己的拷貝 12 ps(new std::string(*rhs.ps)), data(rhs.data){} 13 Demo &operator=(Demo rhs) 14 { 15 swap(*this, rhs); 16 return *this; 17 } 18 std::string *ps; 19 int data; 20 }; 21 22 inline void swap(Demo &lhs, Demo &rhs) 23 { 24 std::cout << "swap" << std::endl; 25 using std::swap; 26 swap(lhs.ps, rhs.ps); // 交換指針 27 swap(lhs.data, rhs.data); // 交換int成員 28 } 29 30 int main() 31 { 32 Demo a, b; 33 *b.ps = "bb"; 34 a = b; 35 std::cout << *a.ps << " " << *b.ps << std::endl; 36 *b.ps = "cc"; 37 std::cout << *a.ps << " " << *b.ps << std::endl; 38 return 0; 39 } View Code我們將swap定義為friend,以便能訪問Demo的所有成員。由于swap的存在就是為了優化代碼,我們將其聲明為inline函數。
注意:與拷貝控制成員不同,swap不是必要的。但是,對于分配了資源的類,定義swap可能是一種很重要的優化手段。
2)swap函數應該調用swap,而不是std::swap
在本例中,數據成員是內置類型的,而內置類型是沒有特定版本的swap的,所以在本例中,對swap調用會調用標準庫std::swap。但是,如果一個類的成員有自己類型特定的swap函數,調用std::swap就是錯誤的了(對于某些類,調用std::swap會進行不必要的拷貝)。
每個swap調用都應該是未加限定的。即,每個調用都應該是swap,而不是std::swap。如果存在類型特定的swap版本,其匹配程度會優于std中定義的版本。
3)在賦值運算符中使用swap
定義swap的類通常用swap來定義它們的賦值運算符。這些運算符使用了一種名為拷貝并交換的技術。這種技術將左側運算對象與右側運算對象的一個副本進行交換。
這個技術自動處理了自賦值情況并且天然就是異常安全的。它通過在改變左側運算對象之前拷貝右側運算對象保證了自賦值的正確,這與我們在原來的賦值運算符中使用的方法是一致的。代碼中唯一可能拋出異常的是拷貝構造函數中的new表達式。如果真發生了異常,它會在我們改變左側運算對象之前發生。
?
三、對象移動
新標準的一個最主要的特性是可以移動而非拷貝對象的能力。很多情況下都會發生對象拷貝,在其中某些情況下,對象拷貝后就立即被銷毀了。在這些情況下,移動而非拷貝對象會大幅度提升性能。
在舊C++標準中,沒有直接的方法移動對象。因此,在舊的標準庫中,容器中所保存的類必須是可拷貝的。但在新標準中,我們可以用容器保存不可拷貝的類型,只要它們能被移動即可。
?
1、右值引用
為了支持移動操作,新標準引入了一種新的引用類型——右值引用。所謂右值引用就是必須綁定到右值的引用。我們通過&&而不是&來獲得右值引用。右值引用有一個重要的性質——只能綁定到一個將要銷毀的對象。
一般而言,一個左值表達式表示的是一個對象的身份(在內存中的位置),一個右值表達式表示的是對象的值(內容)。
一個右值引用是某個對象的另一個名字。對于常規引用(為了與右值引用區分開,我們稱之為左值引用),我們不能將其綁定到要求轉換的表達式、字面常量或是返回右值的表達式。右值引用有著完全相反的綁定特性:我們可以將一個右值引用綁定到這類表達式上,但不能將一個右值引用直接綁定到一個左值上:
1 #include <iostream> 2 #include <string> 3 #include <memory> 4 5 int main() 6 { 7 int i = 42; 8 int &r = i; // r引用i 9 //int &&r = i; // 錯誤:不能將一個右值引用綁定到一個左值上 10 //int &r2 = i * 42; // 錯誤:i*42是一個右值 11 const int &r3 = i * 42; // 可以將一個const引用綁定到一個右值上 12 int &&rr2 = i * 42; // 將rr2綁定到右值上 13 return 0; 14 } View Code返回左值引用的函數,連同賦值、下標、解引用和前置遞增/遞減運算符,都是返回左值表達式的例子。我們可以將一個左值引用綁定到這類表達式的結果上。
返回非左值引用類型的函數,連同算術、關系、位及后置遞增/遞減運算符,都生成右值。我么不能將一個左值引用綁定到這類表達式上,但我們可以將一個const的左值引用綁定或者一個右值引用綁定到這類表達式上。
1)左值持久;右值短暫
左值有持久的狀態,而右值要么是字面值常量,要么是在表達式求值過程中創建的臨時對象。
由于右值引用只能綁定到臨時對象,我們得知:
- 所引用的對象將要被銷毀。
- 該對象沒有其他用戶。
這兩個特性意味著:使用右值引用的代碼可以自由地接管所引用的對象的資源。
2)變量是左值
變量可以看作是只有一個運算對象而沒有運算符的表達式,變量表達式都是左值。因此,我們不能將一個右值引用綁定到一個右值引用類型的變量上。
1 #include <iostream> 2 #include <string> 3 #include <memory> 4 5 int main() 6 { 7 int &&r1 = 42; // 字面值常量是右值 8 //int &&r2 = r1; // 錯誤:表達式r1是左值 9 return 0; 10 } View Code3)標準庫move函數
雖然不能將一個右值引用直接綁定到一個左值上,但我們可以顯示地將一個左值轉換為對應的右值引用類型。我們還可以通過調用一個名為move的新標準庫函數來獲得綁定到左值上的右值引用,此函數位于頭文件utility中。
1 #include <iostream> 2 #include <string> 3 #include <utility> 4 5 int main() 6 { 7 int &&r1 = 42; // 字面值常量是右值 8 int &&r2 = std::move(r1); 9 return 0; 10 } View Codemove調用告訴編譯器:我們有一個左值,但我們希望像一個右值一樣處理它。調用move就意味著承諾:除了對移后源對象(在這里指r1)賦值或銷毀它外,我們將不再使用它。在調用move后,我們不能對移后源對象的值做任何假設。
注意:使用move的代碼應該使用std::move而不是move。這樣做可以避免潛在的名字沖突。
?
2、移動構造函數和移動賦值運算符
類似string類(及其他標準庫類),如果我們自己的類也同時支持移動和拷貝,那么也能從中受益。為了讓我們自己的類型支持移動操作,需要為其定義移動構造函數和移動賦值運算符。這兩個成員類似對應的拷貝操作,但它們從對象“竊取”資源而不是拷貝資源。
類似拷貝構造函數,移動構造函數的第一個參數是該類類型的一個引用。不同于拷貝構造函數的是,這個引用參數在移動構造函數中是一個右值引用。與拷貝構造函數一樣,任何額外的參數都必須由默認實參。
除了完成資源移動,移動構造函數還必須確保移后源對象處于這樣一個狀態——銷毀它是無害的。特別是,一旦資源完成移動,源對象必須不再指向被移動的資源——這些資源的所有權已經歸屬新創建的對象。
? 作為一個例子,我們為StrVec類定義移動構造函數:
1 StrVec::StrVec(StrVec &&s)noexcept // 移動操作不應拋出任何異常 2 // 成員初始化器接管s中的資源 3 : elements(s.elements), first_free(s.first_free), cap(s.cap) 4 { 5 // 令s進入這樣的狀態——對其運行析構函數是安全的 6 s.elements = s.first_free = s.cap = nullptr; 7 } View Code與拷貝構造函數不同,移動構造函數不分配任何內存;它接管給定的StrVec中的內存。在接管內存之后,它將給定對象中的指針都置為nullptr。這樣就完成了從給定對象的移動操作,此對象將繼續存在。最終,移后源對象會被銷毀,意味著將在其上運行析構函數。
1)移動操作、標準庫容器和異常
由于移動操作“竊取”資源,它通常不分配任何資源。因此,移動操作通常不會拋出任何異常。當編寫一個不拋出異常的移動操作時,我們應該將此事通知標準庫。我們將看到,除非標準庫知道我們的移動構造函數不會拋出異常,否則它會認為移動我們的類對象時可能會拋出異常,并且為了處理這種可能性而做一些額外的操作。
一種通知標準庫的方法是在我們的構造函數中指明noexcept。noexcept是新標準引入的,是承諾一個函數不拋出異常的方法。我們在一個函數的參數列表后指定noexcept。在一個構造函數中,noexcept出現在參數列表和初始化列表開始的冒號之間。
必須在類頭文件的聲明中和定義中(如果在定義在類外的話)都指定noexcept。
不拋出異常的移動構造函數和移動賦值運算符必須標記為noexcept。
2)移動賦值運算符
移動賦值運算符執行與析構函數和移動構造函數相同的工作。與移動構造函數一樣,如果我們的移動賦值運算符不拋出任何異常,我們就應該將它標記為noexcept。類似拷貝賦值運算符,移動賦值運算符必須正確處理自賦值。我們不能在使用右側運算對象的資源之前就釋放左側運算對象的資源(可能是相同的資源)。
1 StrVec &StrVec::operator=(StrVec &&rhs)noexcept 2 { 3 if (this != &rhs) 4 { 5 free(); 6 elements = rhs.elements; 7 first_free = rhs.first_free; 8 cap = rhs.cap; 9 rhs.elements = rhs.first_free = rhs.cap = nullptr; 10 } 11 return *this; 12 } View Code3)移后源對象必須可析構
從一個對象移動數據并不會銷毀此對象,但有時在移動操作完成后,源對象會被銷毀。因此,當我們編寫一個移動操作時,必須確保移后源對象進入一個可析構的狀態。
除了將移后源對象置為析構安全的狀態之外,移動操作還必須確保對象仍然是有效的。一般來說,對象有效就是指可以安全地為其賦予新值或者可以安全地使用而不依賴其當前值。另一方面,移動操作對移后源對象中留下的值沒有任何要求。因此,我們的程序不應該依賴于移后源對象中的數據。
4)合成的移動操作
只有當一個類沒有定義任何自己版本的拷貝控制成員,且類的每個非static數據成員都可以移動時,編譯器才會為它合成移動構造函數或移動賦值運算符。編譯器可以移動內置類型的成員。如果一個成員是類類型,且該類有對應的移動操作,編譯器也能移動這個成員。
1 #include <iostream> 2 #include <string> 3 4 // 編譯器會為X和HasX合成移動操作 5 class X { 6 public: 7 int i; // 內置類型可以移動 8 std::string s; // string定義了自己的移動操作 9 }; 10 class HasX { 11 public: 12 X mem;// X有合成的移動操作 13 }; 14 int main() 15 { 16 X x, x2 = std::move(x); // 使用合成的移動構造函數 17 HasX hx, hx2 = std::move(hx); // 使用合成的移動構造函數 18 return 0; 19 } View Code與拷貝操作不同,移動操作永遠也不會隱式地定義為刪除的函數。但是,如果我們顯式地要求編譯器生成=default的移動操作,且編譯器不能移動所有成員,則編譯器會將移動操作定義為刪除的函數。移動操作定義為刪除的函數的原則:
- 與拷貝構造函數不同,移動構造函數被定義為刪除的函數的條件是:有類成員定義了自己的拷貝構造函數且未定義移動構造函數,或者是有類成員未定義自己的拷貝構造函數且編譯器不能為其合成移動構造函數。移動賦值運算符的情況類似。
- 如果有類成員的移動構造函數或移動賦值運算符被定義為刪除的或是不可訪問的,則類的移動構造函數或移動賦值運算符被定義為刪除的。
- 類似拷貝構造函數,如果類的析構函數被定義為刪除的或不可訪問的,則類的移動構造函數被定義為刪除的。
- 類似拷貝賦值運算符,如果有類成員是const的或引用,則類的移動賦值運算符被定義為刪除的。
定義了一個移動構造函數或移動賦值運算符的類必須定義自己的拷貝操作。否則,這些成員默認地被定義為刪除的。
5)移動右值,拷貝左值
如果一個類既有移動構造函數,也有拷貝構造函數,編譯器使用普通的函數匹配規則來確定使用哪個構造函數。賦值的情況類似。
6)如果沒有移動構造函數,右值也被拷貝
如果一個類有一個拷貝構造函數但未定義移動構造函數,會發生什么呢?在此情況下,編譯器不會合成移動構造函數,這意味著此類將有拷貝構造函數但不會有移動構造函數。如果一個類沒有移動構造函數,函數匹配規則保證該類型的對象會被拷貝,即使我們試圖通過調用move來移動它們時也是如此,其對象是通過拷貝構造函數來“移動”的??截愘x值運算符和移動移動賦值運算符的情況類似。
7)拷貝并交換賦值運算符和移動操作
我們的Demo版本定義了一個拷貝并交換賦值運算符,它是函數匹配和移動操作間相互關系的一個很好的示例。
1 #include <iostream> 2 #include <string> 3 #include <memory> 4 5 class Demo 6 { 7 friend void swap(Demo &, Demo &); 8 public: 9 Demo(const std::string &s = std::string()) : 10 ps(new std::string(s)){} 11 Demo(const Demo &rhs) : 12 ps(new std::string(*rhs.ps)) { 13 std::cout << "拷貝構造函數" << std::endl; 14 } 15 Demo(Demo &&rhs)noexcept :ps(rhs.ps) { 16 rhs.ps = nullptr; 17 std::cout << "移動構造函數" << std::endl; 18 } 19 Demo &operator=(Demo rhs) 20 { 21 std::cout << "=" << std::endl; 22 swap(*this, rhs); 23 return *this; 24 } 25 ~Demo() { 26 delete ps; 27 } 28 std::string *ps; 29 }; 30 31 inline void swap(Demo &lhs, Demo &rhs) 32 { 33 std::cout << "swap" << std::endl; 34 using std::swap; 35 swap(lhs.ps, rhs.ps); // 交換指針 36 } 37 38 int main() 39 { 40 Demo hp, hp2; 41 hp = hp2; // hp2是一個左值;hp2通過拷貝構造函數調用 42 std::cout << "--------------------" << std::endl; 43 hp = std::move(hp2); // 移動構造函數移動hp2 44 return 0; 45 } View Code觀察賦值運算符。此運算符有一個非引用參數,這意味著此參數要進行拷貝初始化。依賴于實參的類型,拷貝初始化要么使用拷貝構造函數,要么使用移動構造函數——左值被拷貝,右值被移動。因此,單一的賦值運算符就實現了拷貝賦值運算符和移動賦值運算符兩種功能。
8)移動迭代器
先標準庫中定義了一種移動迭代器適配器。一個移動迭代器通過改變給定迭代器的解引用運算符的行為來適配此迭代器。一般來說,一個迭代器的解引用運算符返回一個指向元素的左值。與其他迭代器不同,移動迭代器的解引用運算符生成一個右值引用。
我們通過調用標準庫的make_move_iterator函數將一個普通迭代器轉換為一個移動迭代器。此函數接受一個迭代器參數,返回一個移動迭代器。原迭代器的所有其他操作在移動迭代器中都照常工作。
1 void StrVec::reallocate() 2 { 3 /* 4 // 不使用移動迭代器的版本 5 auto newcapacity = size() ? 2 * size() : 1; 6 auto newdata = alloc.allocate(newcapacity); 7 auto dest = newdata; 8 auto elem = elements; 9 for (std::size_t i = 0; i != size(); ++i) 10 alloc.construct(dest++, std::move(*elem++)); 11 free(); 12 elements = newdata; 13 first_free = dest; 14 cap = elements + newcapacity;*/ 15 // 使用移動迭代器的版本 16 auto newcapacity = size() ? 2 * size() : 1; 17 auto first = alloc.allocate(newcapacity); 18 auto last = std::uninitialized_copy(std::make_move_iterator(begin()), std::make_move_iterator(end()), first); 19 free(); // 釋放舊空間 20 elements = first; 21 first_free = last; 22 cap = elements + newcapacity; 23 } View Code值得注意的是,標準庫不保證哪些算法適用于移動迭代器,哪些不適用。由于移動一個對象可能銷毀掉原對象,因此你只有在確信算法在為一個元素賦值或將其傳遞給一個用戶定義的函數后不再訪問它時,才能將移動迭代器傳遞給算法。
?
3、右值引用和成員函數
除了構造函數之外,如果一個成員函數同時提供拷貝和移動版本,它也能從中受益。這種允許移動的成員函數通常使用與拷貝/移動構造函數和賦值運算符相同的參數模式——一個版本接受一個指向const的左值引用,第二個版本接受一個指向非const的右值引用。
一般來說,我們不需要為函數操作定義接受一個const X&&或是一個普通的X&參數的版本。當我們希望實參“竊取”數據時,通常傳遞一個右值引用。為了達到這一目的,實參不能是const的。類似的,從一個對象進行拷貝的操作不應該改變該對象。因此,通常不需要定義一個接受普通的X&參數版本。
我們為StrVec類定義另一個版本的push_back:
1 void StrVec::push_back(const std::string &s) 2 { 3 std::cout << "push_back--copy" << std::endl; 4 check_n_alloc(); 5 alloc.construct(first_free++, s); // 在first_free指向的元素中構造s的一個副本 6 } 7 8 void StrVec::push_back(std::string &&s) 9 { 10 std::cout << "push_back--move" << std::endl; 11 check_n_alloc(); 12 alloc.construct(first_free++, std::move(s)); 13 } View Code當我們調用push_back時,實參類型決定了新元素是拷貝還是移動到容器中:
1 #include <iostream> 2 #include <string> 3 #include "header/StrVec.h" 4 5 // 靜態變量在類外一定要定義 6 std::allocator<std::string> StrVec::alloc; 7 int main() 8 { 9 StrVec vec; 10 std::string s = "hello world"; 11 vec.push_back(s); // 調用push_back(const std::string &) 12 vec.push_back("QAQ"); // 調用push_back(std::string &&) 13 return 0; 14 } View Code1)右值和左值引用成員函數
通常我們在一個對象上調用成員函數,而不管該對象是一個左值還是一個右值。例如:
1 #include <iostream> 2 #include <string> 3 4 int main() 5 { 6 std::string s1 = "a value", s2 = "annother"; 7 auto n = (s1 + s2).find('a'); // 在一個string右值上調用find成員 8 std::cout << n << std::endl; 9 s1 + s2 = "wow"; // 對一個右值進行了賦值 10 return 0; 11 } View Code在舊標準中,我們沒辦法阻止這種使用方式。為了維持向后兼容性,新標準庫類仍然允許向右值賦值。但是,我們可能希望自己的類中阻止這種用法。在此情況下,我們希望強制左側運算對象(即this執行的對象)是一個左值。
我們希望指出this的左值/右值屬性的方式與定義const成員函數相同,即,在參數列表后放置一個引用限定符。引用限定符可以是&或&&,分別指出this可以指向一個左值或右值。類似const限定符,引用限定符只能用于非static成員函數,且必須同時出現在函數的聲明和定義中。
1 class Foo { 2 public: 3 Foo &operator=(const Foo &)&; // 只能向可修改的左值賦值 4 }; 5 Foo &Foo::operator=(const Foo &rhs)& 6 { 7 //指向將rhs賦予本對象的操作 8 return *this; 9 } View Code? 一個函數可以同時用const和引用限定。在此情況下,引用限定符必須跟隨在const限定符之后。
1 class Foo { 2 public: 3 Foo some_mem()const &; 4 }; View Code2)重載和引用函數
? 就像一個成員函數可以根據是否有const來區分其重載版本一樣,引用限定符也可以區分重載版本。而且,我們可以綜合利用const來區分一個成員函數的重載版本。
1 #include <iostream> 2 #include <string> 3 #include <vector> 4 #include <algorithm> 5 6 class Foo { 7 public: 8 Foo sorted() && ; // 可用于改變的右值 9 Foo sorted() const &; // 可用于任何類型的Foo 10 private: 11 std::vector<int> data; 12 }; 13 // 本對象為右值,因此可以原址改變 14 Foo Foo::sorted() && 15 { 16 sort(data.begin(), data.end()); 17 return *this; 18 } 19 // 本對象是const或是一個左值,哪種情況我們都不能對其進行原址排序 20 Foo Foo::sorted()const& 21 { 22 Foo ret(*this); 23 sort(ret.data.begin(), ret.data.end()); // 排序副本 24 return ret; // 返回副本 25 } 26 27 int main() 28 { 29 30 return 0; 31 } View Code當我們定義const成員函數時,可以定義兩個版本,唯一的差別是一個版本有const限定而另一個沒有。引用限定的函數則不一樣,如果一個成員函數有引用限定符,則具有相同參數列表的所有版本都必須有引用限定符。
轉載于:https://www.cnblogs.com/ACGame/p/10293428.html
總結
以上是生活随笔為你收集整理的拷贝控制——拷贝控制和资源管理,交换操作,对象移动的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: BZOJ4893: 项链分赃 BZOJ
- 下一篇: 富文本编辑器Quill(二)上传图片与视