條款25:考慮寫出一個不拋異常的swap函數
Consider support for a non-throwing swap
swap是一個有趣的函數。最早作為STL的一部分被引入,後來它成為異常安全編程(exception-safeprogramming)的支柱,和用來處理自我賦值可能性的常見機制。因為 swap太有用了,所以正確地實現它非常重要,但是伴隨它不同尋常的重要性而來的,是一系列不同尋常的復雜性。
swap兩個對象的值就是互相把自己的值賦予對方。缺省情況下,swap動作可由標准程序庫提供的swap算法完成,其典型的實現完全符合你的預期:
namespace std {
template<typename T> // std::swap的典型實現,置換a和b的值
void swap(T& a, T& b)
{
T temp(a);
a = b;
b = temp;
}
}
只要你的類型支持拷貝(通過拷貝構造函數和拷貝賦值運算符),缺省的swap實現就能交換類型為T的對象,而不需要你做任何特別的支持工作。它涉及三個對象的拷貝:從a到temp,從 b到a,以及從temp到b。對一些類型來說,這些賦值動作全是不必要的。
這樣的類型中最重要的就是那些由一個指針組成,這個指針指向包含真正數據的類型。這種設計方法的一種常見的表現形式是"pimpl手法"("pointerto implementation")。如果以這種手法設計Widget 類,可能就像這樣:
class WidgetImpl { // 針對Widget數據設計的類
public:
...
private:
int a, b, c; // 可能有很多數據,意味著復制時間很長
std::vector<double> v;
...
};
class Widget { // 這個類使用pimpl手法
public:
Widget(const Widget& rhs);
Widget& operator=(const Widget& rhs)
{ // 復制Widget時,令其復制WidgetImpl對象
...
*pImpl = *(rhs.pImpl);
...
}
...
private:
WidgetImpl *pImpl; // 指針,所指對象內含Widget數據
};
為了交換這兩個Widget對象的值,我們實際要做的就是交換它們的pImpl指針,但是缺省的交換算法不僅要拷貝三個Widgets,而且還有三個WidgetImpl對象,效率太低了。當交換 Widgets的是時候,我們應該告訴std::swap我們打算執行交換的方法就是交換它們內部的 pImpl指針。這種方法的正規說法是:針對Widget特化std::swap。
class Widget {
public:
...
void swap(Widget&other)
{
using std::swap; // 此聲明是必要的
swap(pImpl, other.pImpl); // 若要置換Widget就置換其pImpl指針
}
...
};
namespace std {
template<> // 這是std::swap針對“T是Widget”的特化版本
void swap<Widget>(Widget& a, Widget& b)
{
a.swap(b); // 若要置換Widget, 調用其swap成員函數
}
}
這個函數開頭的"template<>"表明它是std::swap的一個全特化版本,函數名後面的"<Widget>"表明這一特化版本針對“T是Widget” 而設計。換句話說,當通用的swap模板用於Widgets時,便會啟用這個版本。通常,我們改變std namespace中的內容是不被允許的,但允許為為標准模板(如swap)制造特化版本,使它專屬於我們自己的類(如Widget)。
我們在Widget內聲明一個名為swap的public成員函數去做真正的置換工作,然後特化 std::swap去調用那個成員函數。這樣不僅能夠編譯,而且和STL容器保持一致,所有STL容器都既提供了public swap成員函數,又提供了std::swap的特化來調用這些成員函數。
可是,假設Widget和WidgetImpl是類模板而不是類,或許我們可以試圖將WidgetImpl中的數據類型加以參數化:
template<typename T>
class WidgetImpl { ... };
template<typename T>
class Widget { ... };
以下是方案1:
namespace std {
template<typenameT>
void swap<Widget<T> >(Widget<T>&a, Widget<T>& b)
{ a.swap(b); }
} //錯誤,不合法!
盡管C++允許類模板的偏特化(partialspecialization),但不允許函數模板這樣做。
以下是方案2:
namespace std {
template<typename T> // std::swap的一個重載版本
void swap(Widget<T>& a, Widget<T>&b)
{ a.swap(b); }
} //這也不合法
通常,重載函數模板沒有問題,但是std是一個特殊的命名空間,其規則也比較特殊。它認可完全特化std中的模板,但它不認可在std中增加新的模板(或類,函數,以及其它任何東西)。
正確的方法,既使其他人能調用swap,又能讓我們得到更高效的模板特化版本。我們還是聲明一個非成員swap來調用成員swap,只是不再將那個非成員函數聲明為std::swap的特化或重載。例如,如果Widget相關機能都在namespace WidgetStuff中:
namespace WidgetStuff {
... // 模板化的WidgetImpl等等
template<typename T> // 內含swap成員函數
class Widget { ... };
...
template<typename T> // non-member swap函數,這裡並不屬於std命名空間
voidswap(Widget<T>& a, Widget<T>& b)
{a.swap(b);}
}
現在,如果某處有代碼打算置換兩個Widget對象,調用了swap,C++的名字查找規則將找到WidgetStuff中的Widget專用版本。
現在從客戶的觀點來看一看,假設你寫了一個函數模板來交換兩個對象的值,哪一個swap應該被調用呢?std中的通用版本,還是std中通用版本的特化,還是T專用版本(肯定不在std中)?如果T專用版本存在,則調用它;否則就回過頭來調用std中的通用版本。如下這樣就可以符合你的希望:
template<typename T>
void doSomething(T& obj1, T& obj2)
{
using std::swap; // 令std::swap在此函數內可用
...
swap(obj1, obj2); // 為T類型對象調用最佳swap版本
...
}
當編譯器看到這個swap調用,他會尋找正確的swap版本來調用。如果T是namespaceWidgetStuff中的Widget,編譯器會利用參數依賴查找(argument-dependent lookup)找到WidgetStuff中的swap;如果T專用swap不存在,編譯器將使用std中的swap,這歸功於此函數中的using聲明式使std::swap在此可見。盡管如此,相對於通用模板,編譯器還是更喜歡T專用的std::swap特化,所以如果std::swap對T進行了特化,則特化的版本會被使用。
需要小心的是,不要對調用加以限定,因為這將影響C++挑選適當函數:
std::swap(obj1, obj2); // the wrong way to callswap
這將強制編譯器只考慮std中的swap(包括任何模板特化),因此排除了定義在別處的更為適用的T專用版本被調用的可能性。
總結:
首先,如果swap的缺省實現為你的類或類模板提供了可接受的性能,你不需要做任何事。任何試圖交換類型的對象的操作都會得到缺省版本的支持,而且能工作得很好。
第二,如果swap缺省實現效率不足(這幾乎總是意味著你的類或模板使用了某種pimpl手法),就按照以下步驟來做:
1. 提供一個public的swap成員函數,能高效地交換你的類型的兩個對象值,這個函數應該永遠不會拋出異常。
2. 在你的類或模板所在的同一個namespace中,提供一個非成員的swap,用它調用你的swap成員函數。
3. 如果你寫了一個類(不是類模板),為你的類特化std::swap,並令它調用你的swap 成員函數。
最後,如果你調用swap,確保在你的函數中包含一個using 聲明式使std::swap可見,然後在調用swap時不使用任何namespace修飾符。
警告: 絕不要讓swap的成員版本拋出異常。這是因為swap非常重要的應用之一是為類(以及類模板)提供強大的異常安全(exception-safety)保證。如果你寫了一個swap的自定義版本,那麼,典型情況下你提供一個更有效率的交換值的方法,也保證這個方法不會拋出異常。這兩種swap的特型緊密地結合在一起,因為高效的交換幾乎總是基於內置類型(如pimpl手法下的指針)的操作,而對內置類型的操作絕不會拋出異常。
· 如果 std::swap 對於你的類型來說是低效的,請提供一個 swap 成員函數,並確保你的 swap 不會拋出異常。
· 如果你www.2cto.com提供一個成員 swap,請同時提供一個調用成員swap的非成員swap。對於類(非模板),還要特化 std::swap。
· 調用swap時,請為std::swap使用一個using聲明式,然後在調用 swap時不使用任何namespace修飾符。
· 為用戶定義類型全特化 std 模板是好的,但絕不要試圖往std中加入任何全新的東西。
摘自 pandawuwyj的專欄