# Item 25: 考慮支持不拋異常的 swap
作者:Scott Meyers
譯者:fatalerror99 (iTePub's Nirvana)
發布:http://blog.csdn.net/fatalerror99/
swap 是一個有趣的函數。最早作為 STL 的一部分被引入,后來它成為異常安全編程(exception-safe programming)的支柱(參見 Item 29)和壓制自賦值可能性的通用機制(參見 Item 11)。因為 swap 太有用了,所以正確地實現它非常重要,但是伴隨它的不同尋常的重要性而來的,是一系列不同尋常的復雜性。在本 Item 中,我們就來研究一下這些復雜性究竟是什么樣的以及如何對付它們。
交換兩個對象的值就是互相把自己的值送給對方。缺省情況下,通過標準的交換算法來實現交換是非常成熟的技術。典型的實現完全符合你的預期:
```
namespace std {
template<typename T> // typical implementation of std::swap;
void swap(T& a, T& b) // swaps a's and b's values
{
T temp(a);
a = b;
b = temp;
}
}
```
只要你的類型支持拷貝(通過拷貝構造函數和拷貝賦值運算符),缺省的 swap 實現就能交換你的類型的對象,而不需要你做任何特別的支持工作
可是,缺省的 swap 實現可能不那么酷。它涉及三個對象的拷貝:從 a 到 temp,從 b 到 a,以及從 temp 到 b。對一些類型來說,這些副本全是不必要的。對于這樣的類型,缺省的 swap 就好像讓你坐著快車駛入小巷。
這樣的類型中最重要的就是那些主要由一個指針組成的類型,那個指針指向包含真正數據的另一種類型。這種設計方法的一種常見的表現形式是 "pimpl idiom"("pointer to implementation" ——參見 Item 31)。一個使用了這種設計的 Widget 類可能就像這樣:
```
class WidgetImpl { // class for Widget data;
public: // details are unimportant
...
private:
int a, b, c; // possibly lots of data —
std::vector<double> v; // expensive to copy!
...
};
class Widget { // class using the pimpl idiom
public:
Widget(const Widget& rhs);
Widget& operator=(const Widget& rhs) // to copy a Widget, copy its
{ // WidgetImpl object. For
... // details on implementing
*pImpl = *(rhs.pImpl); // operator= in general,
... // see Items 10, 11, and 12.
}
...
private:
WidgetImpl *pImpl; // ptr to object with this
};
```
為了交換這兩個 Widget 對象的值,我們實際要做的就是交換它們的 pImpl 指針,但是缺省的交換算法沒有辦法知道這些。它不僅要拷貝三個 Widgets,而且還有三個 WidgetImpl 對象,效率太低了。一點都不酷。
當交換 Widgets 的是時候,我們應該告訴 std::swap 我們打算做什么,執行交換的方法就是交換它們內部的 pImpl 指針。這種方法的正規說法是:針對 Widget 特化 std::swap(specialize std::swap for Widget)。下面是一個基本的想法,雖然在這種形式下它還不能通過編譯:
```
namespace std {
template<> // this is a specialized version
void swap<Widget>(Widget& a, // of std::swap for when T is
Widget& b) // Widget; this won't compile
{
swap(a.pImpl, b.pImpl); // to swap Widgets, just swap
} // their pImpl pointers
}
```
這個函數開頭的 "template<>" 表明這是一個針對 std::swap 的完全模板特化(total template specialization)(某些書中稱為 "full template specialization" 或 "complete template specialization" ——譯者注),函數名后面的 "<Widget>" 表明特化是在 T 為 Widget 類型時發生的。換句話說,當通用的 swap 模板用于 Widgets 時,就應該使用這個實現。通常,我們改變 std namespace 中的內容是不被允許的,但允許為我們自己創建的類型(就像 Widget)完全特化標準模板(就像 swap)。這就是我們現在在這里做的事情。
可是,就像我說的,這個函數還不能編譯。那是因為它試圖訪問 a 和 b 內部的 pImpl 指針,而它們是 private 的。我們可以將我們的特化聲明為友元,但是慣例是不同的:讓 Widget 聲明一個名為 swap 的 public 成員函數去做實際的交換,然后特化 std::swap 去調用那個成員函數:
```
class Widget { // same as above, except for the
public: // addition of the swap mem func
...
void swap(Widget& other)
{
using std::swap; // the need for this declaration
// is explained later in this Item
swap(pImpl, other.pImpl); // to swap Widgets, swap their
} // pImpl pointers
...
};
namespace std {
template<> // revised specialization of
void swap<Widget>(Widget& a, // std::swap
Widget& b)
{
a.swap(b); // to swap Widgets, call their
} // swap member function
}
```
這個不僅能夠編譯,而且和 STL 容器保持一致,所有 STL 容器都既提供了 public swap 成員函數,又提供了 std::swap 的特化來調用這些成員函數。
可是,假設 Widget 和 WidgetImpl 是類模板,而不是類,或許因此我們可以參數化存儲在 WidgetImpl 中的數據類型:
```
template<typename T>
class WidgetImpl { ... };
template<typename T>
class Widget { ... };
```
在 Widget 中加入一個 swap 成員函數(如果我們需要,在 WidgetImpl 中也加一個)就像以前一樣容易,但我們特化 std::swap 時會遇到麻煩。這就是我們要寫的代碼:
```
namespace std {
template<typename T>
void swap<Widget<T> >(Widget<T>& a, // error! illegal code!
Widget<T>& b)
{ a.swap(b); }
}
```
這看上去非常合理,但它是非法的。我們試圖部分特化(partially specialize)一個函數模板(std::swap),但是盡管 C++ 允許類模板的部分特化(partial specialization),但不允許函數模板這樣做。這樣的代碼不能編譯(盡管一些編譯器錯誤地接受了它)。
當我們想要“部分特化”一個函數模板時,通常做法是簡單地增加一個重載。看起來就像這樣:
```
namespace std {
template<typename T> // an overloading of std::swap
void swap(Widget<T>& a, // (note the lack of "<...>" after
Widget<T>& b) // "swap"), but see below for
{ a.swap(b); } // why this isn't valid code
}
```
通常,重載函數模板確實很不錯,但是 std 是一個特殊的 namespace,規則對它也有特殊的待遇。它認可完全特化 std 中的模板,但它不認可在 std 中增加新的模板(也包括類,函數,以及其它任何東西)。std 的內容由 C++ 標準化委員會單獨決定,并禁止我們對他們做出的決定進行增加。而且,禁止的方式使你無計可施。打破這條禁令的程序差不多的確可以編譯和運行,但它們的行為是未定義的。如果你希望你的軟件有可預期的行為,你就不應該向 std 中加入新的東西。
因此該怎么做呢?我們還是需要一個方法,既使其他人能調用 swap,又能讓我們得到更高效的模板特化版本。答案很簡單。我們還是聲明一個非成員 swap 來調用成員 swap,只是不再將那個非成員函數聲明為 std::swap 的特化或重載。例如,如果我們的 Widget 相關機能都在 namespace WidgetStuff 中,它看起來就像這個樣子:
```
namespace WidgetStuff {
... // templatized WidgetImpl, etc.
template<typename T> // as before, including the swap
class Widget { ... }; // member function
...
template<typename T> // non-member swap function;
void swap(Widget<T>& a, // not part of the std namespace
Widget<T>& b)
{
a.swap(b);
}
}
```
現在,如果某處有代碼使用兩個 Widget 對象調用 swap,C++ 的名字查找規則(以參數依賴查找(argument-dependent lookup)或 Koenig 查找(Koenig lookup)著稱的特定規則)將找到 WidgetStuff 中的 Widget 專用版本。而這正是我們想要的。
這個方法無論對于類模板還是對于類都能很好地工作,所以看起來我們應該總是使用它。不幸的是,此處還是存在一個需要為類特化 std::swap 的動機(過一會兒我會講到它),所以如果你希望你的 swap 的類專用版本在盡可能多的上下文中都能夠調用(而你也確實這樣做了),你就既要在你的類所在的 namespace 中寫一個非成員版本,又要提供一個 std::swap 的特化版本。
順便提一下,如果你不使用 namespaces,上面所講的一切依然適用(也就是說,你還是需要一個非成員 swap 來調用成員 swap),但是你為什么要把你的類,模板,函數,枚舉(此處作者連用了兩個詞(enum, enumerant),不知有何區別——譯者注)和 typedef 名字都堆在全局 namespace 中呢?你覺得合適嗎?
迄今為止我所寫的每一件事情都適用于 swap 的作成者,但是有一種狀況值得從客戶的觀點來看一看。假設你寫了一個函數模板來交換兩個對象的值:
```
template<typename T>
void doSomething(T& obj1, T& obj2)
{
...
swap(obj1, obj2);
...
}
```
哪一個 swap 應該被調用呢?std 中的通用版本,你知道它必定存在;std 中的通用版本的特化,可能存在,也可能不存在;T 專用版本,可能存在,也可能不存在,可能在一個 namespace 中,也可能不在一個 namespace 中(但是肯定不在 std 中)。究竟該調用哪一個呢?如果 T 專用版本存在,你希望調用它,如果它不存在,就回過頭來調用 std 中的通用版本。如下這樣就可以符合你的希望:
```
template<typename T>
void doSomething(T& obj1, T& obj2)
{
using std::swap; // make std::swap available in this function
...
swap(obj1, obj2); // call the best swap for objects of type T
...
}
```
當編譯器看到這個 swap 調用,他會尋找正確的 swap 版本來調用。C++ 的名字查找規則確保能找到在全局 namespace 或者與 T 同一個 namespace 中的 T 專用的 swap。(例如,如果 T 是 namespace WidgetStuff 中的 Widget,編譯器會利用參數依賴查找(argument-dependent lookup)找到 WidgetStuff 中的 swap。)如果 T 專用 swap 不存在,編譯器將使用 std 中的 swap,這歸功于此函數中的 using declaration 使 std::swap 在此可見。盡管如此,相對于通用模板,編譯器還是更喜歡 T 專用的 std::swap 的特化,所以如果 std::swap 對 T 進行了特化,則特化的版本會被使用。
得到正確的 swap 調用是如此地容易。你需要小心的一件事是不要對調用加以限定,因為這將影響 C++ 確定該調用的函數,如果你這樣寫對 swap 的調用,
```
std::swap(obj1, obj2); // the wrong way to call swap
```
這將強制編譯器只考慮 std 中的 swap(包括任何模板特化),因此排除了定義在別處的更為適用的 T 專用版本被調用的可能性。唉,一些被誤導的程序員就是用這種方法限定對 swap 的調用,這也就是為你的類完全地特化 std::swap 很重要的原因:它使得以這種被誤導的方式寫出的代碼可以用到類型專用的 swap 實現。(這樣的代碼還存在于現在的一些標準庫實現中,所以它將有利于你幫助這樣的代碼盡可能高效地工作。)
到此為止,我們討論了缺省的 swap,成員 swaps,非成員 swaps,std::swap 的特化版本,以及對 swap 的調用,所以讓我們總結一下目前的狀況。
首先,如果 swap 的缺省實現為你的類或類模板提供了可接受的性能,你不需要做任何事。任何試圖交換你的類型的對象的人都會得到缺省版本的支持,而且能工作得很好。
第二,如果 swap 的缺省實現效率不足(這幾乎總是意味著你的類或模板使用了某種 pimpl idiom 的變種),就按照以下步驟來做:
1\. 提供一個能高效地交換你的類型的兩個對象的值的 public 的 swap 成員函數。出于我過一會兒就要解釋的動機,這個函數應該永遠不會拋出異常。
2\. 在你的類或模板所在的同一個 namespace 中提供一個非成員的 swap。用它調用你的 swap 成員函數。
3\. 如果你寫了一個類(不是類模板),就為你的類特化 std::swap。用它也調用你的 swap 成員函數。
最后,如果你調用 swap,請確保在你的函數中包含一個 using declaration 使 std::swap 可見,然后在調用 swap 時不使用任何 namespace 限定條件。
唯一沒有解決的問題就是我的警告——絕不要讓 swap 的成員版本拋出異常。這是因為 swap 的非常重要的應用之一是為類(以及類模板)提供強大的異常安全(exception-safety)保證。Item 29 將提供所有的細節,但是這項技術基于 swap 的成員版本絕不會拋出異常的假設。這一強制約束僅僅應用在成員版本上!它不能夠應用在非成員版本上,因為 swap 的缺省版本基于拷貝構造和拷貝賦值,而在通常情況下,這兩個函數都允許拋出異常。如果你寫了一個 swap 的自定義版本,那么,典型情況下你是為了提供一個更有效率的交換值的方法,你也要保證這個方法不會拋出異常。作為一個一般規則,這兩種 swap 的特型將緊密地結合在一起,因為高效的交換幾乎總是基于內建類型(諸如在 pimpl idiom 之下的指針)的操作,而對內建類型的操作絕不會拋出異常。
Things to Remember
* 如果 std::swap 對于你的類型來說是低效的,請提供一個 swap 成員函數。并確保你的 swap 不會拋出異常。
* 如果你提供一個成員 swap,請同時提供一個調用成員 swap 的非成員 swap。對于類(非模板),還要特化 std::swap。
* 調用 swap 時,請為 std::swap 使用一個 using declaration,然后在調用 swap 時不使用任何 namespace 限定條件。
* 為用戶定義類型完全地特化 std 模板沒有什么問題,但是絕不要試圖往 std 中加入任何全新的東西。
- Preface(前言)
- Introduction(導言)
- Terminology(術語)
- Item 1: 將 C++ 視為 federation of languages(語言聯合體)
- Item 2: 用 consts, enums 和 inlines 取代 #defines
- Item 3: 只要可能就用 const
- Item 4: 確保 objects(對象)在使用前被初始化
- Item 5: 了解 C++ 為你偷偷地加上和調用了什么函數
- Item 6: 如果你不想使用 compiler-generated functions(編譯器生成函數),就明確拒絕
- Item 7: 在 polymorphic base classes(多態基類)中將 destructors(析構函數)聲明為 virtual(虛擬)
- Item 8: 防止因為 exceptions(異常)而離開 destructors(析構函數)
- Item 9: 絕不要在 construction(構造)或 destruction(析構)期間調用 virtual functions(虛擬函數)
- Item 10: 讓 assignment operators(賦值運算符)返回一個 reference to *this(引向 *this 的引用)
- Item 11: 在 operator= 中處理 assignment to self(自賦值)
- Item 12: 拷貝一個對象的所有組成部分
- Item 13: 使用對象管理資源
- Item 14: 謹慎考慮資源管理類的拷貝行為
- Item 15: 在資源管理類中準備訪問裸資源(raw resources)
- Item 16: 使用相同形式的 new 和 delete
- Item 17: 在一個獨立的語句中將 new 出來的對象存入智能指針
- Item 18: 使接口易于正確使用,而難以錯誤使用
- Item 19: 視類設計為類型設計
- Item 20: 用 pass-by-reference-to-const(傳引用給 const)取代 pass-by-value(傳值)
- Item 21: 當你必須返回一個對象時不要試圖返回一個引用
- Item 22: 將數據成員聲明為 private
- Item 23: 用非成員非友元函數取代成員函數
- Item 24: 當類型轉換應該用于所有參數時,聲明為非成員函數
- Item 25: 考慮支持不拋異常的 swap
- Item 26: 只要有可能就推遲變量定義
- Item 27: 將強制轉型減到最少
- Item 28: 避免返回對象內部構件的“句柄”
- Item 29: 爭取異常安全(exception-safe)的代碼
- Item 30: 理解 inline 化的介入和排除
- Item 31: 最小化文件之間的編譯依賴
- Item 32: 確保 public inheritance 模擬 "is-a"
- Item 33: 避免覆蓋(hiding)“通過繼承得到的名字”
- Item 34: 區分 inheritance of interface(接口繼承)和 inheritance of implementation(實現繼承)
- Item 35: 考慮可選的 virtual functions(虛擬函數)的替代方法
- Item 36: 絕不要重定義一個 inherited non-virtual function(通過繼承得到的非虛擬函數)
- Item 37: 絕不要重定義一個函數的 inherited default parameter value(通過繼承得到的缺省參數值)
- Item 38: 通過 composition(復合)模擬 "has-a"(有一個)或 "is-implemented-in-terms-of"(是根據……實現的)
- Item 39: 謹慎使用 private inheritance(私有繼承)
- Item 40: 謹慎使用 multiple inheritance(多繼承)
- Item 41: 理解 implicit interfaces(隱式接口)和 compile-time polymorphism(編譯期多態)
- Item 42: 理解 typename 的兩個含義
- Item 43: 了解如何訪問 templatized base classes(模板化基類)中的名字
- Item 44: 從 templates(模板)中分離出 parameter-independent(參數無關)的代碼
- Item 45: 用 member function templates(成員函數模板) 接受 "all compatible types"(“所有兼容類型”)
- Item 46: 需要 type conversions(類型轉換)時在 templates(模板)內定義 non-member functions(非成員函數)
- Item 47: 為類型信息使用 traits classes(特征類)
- Item 48: 感受 template metaprogramming(模板元編程)
- Item 49: 了解 new-handler 的行為
- Item 50: 領會何時替換 new 和 delete 才有意義
- Item 51: 編寫 new 和 delete 時要遵守慣例
- Item 52: 如果編寫了 placement new,就要編寫 placement delete
- 附錄 A. 超越 Effective C++
- 附錄 B. 第二和第三版之間的 Item 映射