讀書筆記 effective c++ Item 25 實現一個不拋出異常的swap。本站提示廣大學習愛好者:(讀書筆記 effective c++ Item 25 實現一個不拋出異常的swap)文章只能為提供參考,不一定能成為您想要的結果。以下是讀書筆記 effective c++ Item 25 實現一個不拋出異常的swap正文
Swap是一個非常有趣的函數,最初作為STL的一部分來介紹,它已然變成了異常安全編程的中流砥柱(Item 29),也是在拷貝中應對自我賦值的一種普通機制(Item 11)。Swap非常有用,恰當的實現swap是非常重要的,與重要性伴隨而來的是一些並發症。在這個條款中,我們將探索這些並發症以及如何處理它們。
2. swap的傻瓜實現方式及缺陷 2.1 swap函數的默認實現Swap函數就是將兩個對象的值進行交換,可以通過使用標准的swap算法來實現:
1 namespace std { 2 3 template<typename T> // typical implementation of std::swap; 4 5 void swap(T& a, T& b) // swaps a’s and b’s values 6 7 { 8 9 T temp(a); 10 11 a = b; 12 13 b = temp; 14 15 } 16 17 }
只要你的類型支持拷貝(拷貝構造函數和拷貝賦值運算符),默認的swap實現不需要你做一些特別的工作來支持它。
2.2 swap函數默認實現的缺陷——有可能效率低然而,默認的swap實現也許並沒有讓你激動,它包括三次拷貝:a 拷貝到temp,b拷貝到a, temp拷貝到b。對於一些類型來說,這些拷貝不是必須的,默認的swap將你從快車道拉到了慢車道。
這些不需要拷貝的類型內部通常包含了指針,指針指向包含真實數據的其他類型。使用這種設計方法的一個普通的例子就是“pimpl idiom”(指向實現的指針 Item 31).舉個例子:
1 class WidgetImpl { // class for Widget data; 2 3 public: // details are unimportant 4 5 ... 6 7 private: 8 9 int a, b, c; // possibly lots of data — 10 11 std::vector<double> v; // expensive to copy! 12 13 ... 14 15 }; 16 17 class Widget { // class using the pimpl idiom 18 19 public: 20 21 Widget(const Widget& rhs); 22 23 Widget& operator=(const Widget& rhs) // to copy a Widget, copy its 24 25 { // WidgetImpl object. For 26 27 ... // details on implementing 28 29 *pImpl = *(rhs.pImpl); // operator= in general, 30 31 ... // see Items 10, Item 11, and Item 12. 32 33 } 34 35 ... 36 37 private: 38 39 WidgetImpl *pImpl; // ptr to object with this 40 41 }; // Widget’s data
為了交換兩個Widget對象的值,我們實際上唯一需要做的是交換兩個pImpl指針,但是默認的swap算法沒有辦法能夠獲知這些。它不僅拷貝了三個Widget對象,還拷貝了三個WidgetImpl對象。非常沒有效率,也不令人雞凍。
3. 如何實現一個高效的swap 3.1 為普通類定義全特化版本swap我們需要做的就是告訴std::swap當Widget對象被swap的時候,執行swap的方式是swap內部的pImpl指針。也就是為Widget定制一個std::swap。這是最基本的想法,看下面的代碼,但是不能通過編譯。。
1 namespace std { 2 3 template<> // this is a specialized version 4 5 void swap<Widget>(Widget& a, // of std::swap for when T is 6 7 Widget& b) // Widget 8 9 { 10 11 swap(a.pImpl, b.pImpl); // to swap Widgets, swap their 12 13 } // pImpl pointers; this won’t 14 15 compile 16 17 }
開始的”templpate<>”說明這是對std::swap的模板全特化(total template specializaiton),名字後面的”<Widget>”是說明這個特化只針對T為Widget類型。換句話說,當泛化的swap模板被應用到Widget類型時,應該使用上面的實現方法。一般來說,我們不允許修改std命名空間的內容,但是卻允許使用我們自己創建的類型對標准模板進行全特化。
但是這個函數不能編譯通過。這是因為它嘗試訪問a和b中的pImpl指針,它們是private的。我們可以將我們的特化函數聲明成friend,但是傳統做法卻是這樣:在Widget中聲明一個真正執行swap的public成員函數swap,讓std::swap調用成員函數:
1 class Widget { // same as above, except for the 2 3 public: // addition of the swap mem func 4 5 ... 6 7 void swap(Widget& other) 8 9 { 10 11 using std::swap; // the need for this declaration 12 13 // is explained later in this Item 14 15 swap(pImpl, other.pImpl); // to swap Widgets, swap their 16 17 } // pImpl pointers 18 19 ... 20 21 }; 22 23 namespace std { 24 25 template<> // revised specialization of 26 27 void swap<Widget>(Widget& a, // std::swap 28 29 Widget& b) 30 31 { 32 33 a.swap(b); // to swap Widgets, call their 34 35 } // swap member function 36 37 }
這種做法不僅編譯能通過,同STL容器一致,它們都同時為swap提供了public成員函數版本和調用成員函數的std::swap版本。
3.2 為模板類定義偏特化版本swap然而假設Widget和WidgetImpl換成了類模版,我們就將存儲在WidgetImpl中的數據類型替換成一個模板參數:
1 template<typename T> 2 3 class WidgetImpl { ... }; 4 5 template<typename T> 6 7 class Widget { ... };
在Widget中實現一個swap成員函數和原來一樣簡單,但是std::swap的特化遇到了麻煩。下面是我們想寫出來的:
1 namespace std { 2 3 template<typename T> 4 5 void swap<Widget<T> >(Widget<T>& a, // error! illegal code! 6 7 Widget<T>& b) 8 9 { a.swap(b); } 10 11 }
上面的代碼看上去完全合理,但卻是不合法的。我們嘗試偏特化(partially specialize)一個模板(std::swap),雖然允許對類模版進行偏特化,卻不允許對函數模板進行偏特化。因此這段代碼不能通過編譯(雖然有些編譯器錯誤的通過了編譯)。
當你想“偏特化”一個函數模板的時候,常見的方法是添加一個重載函數。像下面這樣:
1 namespace std { 2 3 template<typename T> // an overloading of std::swap 4 5 void swap(Widget<T>& a, // (note the lack of “<...>” after 6 7 Widget<T>& b) // “swap”), but see below for 8 9 { a.swap(b); } // why this isn’t valid code 10 11 }
一般來說,對函數模板進行重載是可以的,但是std是一個特殊的命名空間,使用它的規則也很特殊。在std中進行全特化是可以的,但是添加新的模板(類,函數或其他任何東西)不可以。Std的內容完全由C++標准委員會來決定。越過這條線的程序肯定可以通過編譯並且能運行,但是行為未定義。如果你想你的軟件有可預測的行為,不要向std中添加新東西。
那該怎麼做呢?我們仍然需要一種方式來讓其他人調用我們的高效的模板特化版本的swap。答案很簡單。我們仍然聲明一個調用成員函數swap的非成員swap,但我們不將非成員函數聲明為std::swap的特化或者重載。舉個例子,和 Widget相關的功能被定義在命名空間WidgetStuff中,像下面這樣:
1 namespace WidgetStuff { 2 3 ... // templatized WidgetImpl, etc. 4 5 template<typename T> // as before, including the swap 6 7 class Widget { ... }; // member function 8 9 ... 10 11 template<typename T> // non-member swap function; 12 13 void swap(Widget<T>& a, // not part of the std namespace 14 15 Widget<T>& b) 16 17 { 18 19 a.swap(b); 20 21 } 22 23 }
現在,如果在任何地方調用swap,C++ 中的名字搜尋策略(name lookup rules)將會在WidgetStuff中搜尋Widget的指定版本。這正是我們需要的。
4. 普通類中swap的特化版本和非成員函數版本都需要提供這種方法對類同樣有效,所以看上去我們應該在任何情況下都使用它。不幸的是,你還需要為類提供特化的std::swap(稍後解釋)版本,所以如果你想在盡可能多的上下文環境中調用swap的類特定版本,你需要同時在類命名空間中定義swap的非成員函數版本和std::swap的特化版本。
5. 調用swap時的搜尋策略至今為止我已經實現的都要從屬於swap的作者,但從客戶角度來看有一種情況值得注意。假設你正在實現一個函數模板,函數中需要對兩個對象的值進行swap:
1 template<typename T> 2 3 void doSomething(T& obj1, T& obj2) 4 5 { 6 7 ... 8 9 swap(obj1, obj2); 10 11 ... 12 13 }
他會調用swap的哪個版本?已存的std中的版本?可能存在也可能不存在的std中的特化版本?還是可能存在也可能不存在的,可能在一個命名空間內也可能不在一個命名空間內(肯定不應該在std中)T特定版本?你所需要的是如果有的話就調用一個T特定版本,沒有的話就調用std中的普通版本。下面來實現你的需求:
1 template<typename T> 2 3 void doSomething(T& obj1, T& obj2) 4 5 { 6 7 using std::swap; // make std::swap available in this function 8 9 ... 10 11 swap(obj1, obj2); // call the best swap for objects of type T 12 13 ... 14 15 }
當編譯器看到了對swap的調用,它們會尋找swap的正確版本。C++名字搜尋策略先在全局范圍內或者同一個命名空間內搜尋swap的T特定版本。(例如,如果T是命名空間WidgetStuff中的Widget,編譯器會用參數依賴搜尋(argument-dependent lookup)在WidgetStuff中尋找swap).如果沒有T特定的swap版本存在,編譯器會使用std中的swap版本,多虧了using std::swap使得std::swap在函數中可見。但是編譯器更喜歡普通模板std::swap上的T指定特化版本,因此如果std::swap已經為T特化過了,特化版本將會調用。
6. 調用swap時不要加std限定符因此調用正確的swap版本很容易。一件你需要注意的事情是不要對調用進行限定,因為這會影響c++決定調用哪個函數。舉個例子,如果你像下面這樣調用swap:
1 std::swap(obj1, obj2); // the wrong way to call swap
你強制編譯器只考慮std中的swap版本(包含所有模板特化版本),這樣就調不到在其他地方定義的更加合適的T特定版本了(如果有的話)。一些被誤導的程序員確實就對swap的調用進行了這種限定,因此為你的類對std::swap進行全特化很重要:它使得被誤導的程序員即使使用錯誤的調用方式(加std限定)也能夠調用特定類型的swap版本。
7. 實現swap步驟小結到現在我們已經討論了默認swap,成員函數swap,非成員函數swap以及std::swap的特化版本,並且討論了對swap的調用,讓我們總結一下:
首先,如果為你的類或者類模版提供的swap默認實現版本在效率上可以滿足你,你就什麼都不需要做。任何人嘗試對你定義類型的對象進行swap,只要調用默認版本就可以了,這會工作的很好。
其次,如果swap的默認實現在效率上達不到你的要求(通常就意味著你的類或者類模板在使用同指向實現的指針(pimpl idiom)類似的變量),那麼按照下面的去做:
最後,如果你正在調用swap,確保在你的函數中include一個using聲明來使得std::swap是可見的,然後調用swap時不要加std命名空間對其進行限定。
8. 最後的警告——不要讓成員函數swap拋出異常我最後的警告是永遠不要讓swap成員函數版本拋出異常。因為swap的一個最有用的地方就是幫助類(或類模版)提供強有力的異常安全保證。Item 29中有詳細解釋,其中的技術也是建立在swap成員函數版本不會拋出異常的假設之上的。這個約束只針對成員函數版本!而不針對非成員函數版本,因為swap的默認版本是基於拷貝構造函數和拷貝賦值運算符的,而一般情況下,這兩個函數都允許拋出異常。當你實現一個swap的個性化版本,你就不單單提供了對值進行swap的高效方法;你同時提供了一個不會拋出異常的函數。作為通用規則,swap的這兩個特性總是會在一起的,因為高效的swap通常是建立在對內建類型進行操作的基礎之上的(像底層的指向實現的指針),而內建類型永遠不會拋出異常。
9. 總結