條款25 考慮寫出乙個不拋異常的swap函式

2021-06-30 16:57:13 字數 4008 閱讀 5929

總結:

如果 std::swap 對於你的型別來說是低效的,請提供乙個 swap 成員函式,並確保你的 swap 不會丟擲異常。

如果你提供乙個成員 swap,請同時提供乙個呼叫成員swap的非成員swap。對於類(非模板),還要特化 std::swap。

呼叫swap時,請為std::swap使用乙個using宣告式,然後在呼叫 swap時不使用任何namespace修飾符。

為「使用者定義型別」全特化 std 模板是好的,但絕不要嘗試在std中加入任何全新的東西。

swap是乙個有趣的函式。最早作為stl的一部分被引入,後來它成為異常安全程式設計(exception-safeprogramming)的支柱,和用來處理自我賦值可能性的常見機制。因為 swap太有用了,所以正確地實現它非常重要,但是伴隨它不同尋常的重要性而來的,是一系列不同尋常的複雜性。

swap兩個物件的值就是互相把自己的值賦予對方。預設情況下,swap動作可由標準程式庫提供的swap演算法完成,其典型的實現完全符合你的預期:

namespace std 

}

只要你的型別支援拷貝(通過拷貝建構函式和拷貝賦值運算子),預設的swap實現就能交換型別為t的物件,而不需要你做任何特別的支援工作。它涉及三個物件的拷貝:從a到temp,從 b到a,以及從temp到b。對一些型別來說,這些賦值動作全是不必要的

這樣的型別中最重要的就是那些由乙個指標組成,這個指標指向包含真正資料的型別。這種設計方法的一種常見的表現形式是"pimpl手法"("pointerto implementation")。如果以這種手法設計widget 類,可能就像這樣:

class widgetimpl ;

class widget

...private:

widgetimpl *pimpl; // 指標,所指物件內含widget資料

};

為了交換這兩個widget物件的值,我們實際要做的就是交換它們的pimpl指標,但是預設的交換演算法不僅要拷貝三個widgets,而且還有三個widgetimpl物件,效率太低了。當交換 widgets的是時候,我們應該告訴std::swap我們打算執行交換的方法就是交換它們內部的 pimpl指標。這種方法的正規說法是:針對widget特化std::swap。如下令widget宣告乙個名為是swap的public成員做真正的交換工作,然後將std::swap特化,令它呼叫該成員函式

class widget 

...};namespace std

}

這樣不僅能夠編譯,而且和stl容器保持一致,所有stl容器都既提供了public swap成員函式,又提供了std::swap的特化來呼叫這些成員函式。這個函式開頭的"template<>"表明它是std::swap的乙個全特化版本,函式名後面的""表明這一特化版本針對「t是widget」 而設計。換句話說,當通用的swap模板用於widgets時,便會啟用這個版本。通常,我們改變std namespace中的內容是不被允許的,但允許為為標準模板(如swap)製造特化版本,使它專屬於我們自己的類(如widget)。

可是,假設widget和widgetimpl是類模板而不是類,或許我們可以試圖將widgetimpl中的資料型別加以引數化:

templateclass widgetimpl ;

templateclass widget ;

可能的方法一:在widet內(以及widgetimpl內,如果需要的話)放乙個swap成員函式就像以往一樣簡單,但是我們卻在特化std::swap時遇到亂流。我們想寫成這樣:

namespace std 

} //錯誤,不合法!

我們企圖偏特化乙個函式模板,這行不通;c++只允許偏特化類模板。

可能的方法二:想偏特化乙個函式模板,慣常做法是簡單地為它新增乙個過載版本:

namespace std 

} //這也不合法

通常,過載函式模板沒有問題,但是std是乙個特殊的命名空間,其規則也比較特殊。它認可完全特化std中的模板,但它不認可在std中增加新的模板(或類,函式,以及其它任何東西)。std的內容完全由c++標準委員會決定,其禁止我們膨脹那些已經宣告好的東西。不要新增新東西到std內。

正確的方法,既使其他人能呼叫swap,又能讓我們得到更高效的模板特化版本。我們還是

宣告乙個非成員swap來呼叫成員swap,只是不再將那個非成員函式宣告為std::swap的特化或過載。例如,如果widget相關機能都在namespace widgetstuff中:

namespace widgetstuff ;

...template//非成員swap函式,這裡並不屬於std命名空間

void swap(widget& a, widget& b)

}

現在,如果某處有**打算置換兩個widget物件,呼叫了swap,c++的名字查詢規則將找到widgetstuff中的widget專用版本

現在從客戶的觀點來看一看,假設你寫了乙個函式模板來交換兩個物件的值,哪乙個swap應該被呼叫呢?std中的通用版本,還是std中通用版本的特化,還是t專用版本(肯定不在std中)?如果t專用版本存在,則呼叫它;否則就回過頭來呼叫std中的通用版本。如下這樣就可以符合你的希望:

templatevoid dosomething(t& obj1, t& obj2)

當編譯器看到這個swap呼叫,他會尋找正確的swap版本來呼叫。如果t是namespace widgetstuff中的widget,編譯器會利用引數依賴查詢(argument-dependent lookup)找到widgetstuff中的swap;如果t專用swap不存在,編譯器將使用std中的swap,這歸功於此函式中的using宣告式使std::swap在此可見。儘管如此,相對於通用模板,編譯器還是更喜歡t專用的std::swap特化,所以如果std::swap對t進行了特化,則特化的版本會被使用。

需要小心的是,不要對呼叫加以限定,因為這將影響c++挑選適當函式:

std::swap(obj1, obj2);   //這是錯誤的swap呼叫方式
這將強制編譯器只考慮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手法下的指標)的操作,而對內建型別的操作絕不會丟擲異常。

條款25 考慮寫出乙個不丟擲異常的swap函式

swap原本只是stl的一部分,後面成為異常安全程式設計的脊柱,及處理自我賦值安全性的乙個常見機制。例子 標準程式庫提供的swap演算法 namespace s td 要求 型別t支援copying 通過copying建構函式和copyassignment操作符完成 上述 主要涉及三個物件的複製,但...

條款25 考慮寫出乙個不丟擲異常的swap函式

首先本篇部落格的主要思想是 系統自帶的swap函式有時候不能滿足我們的需求,所以在一些情況下我們就需要自己去寫swap函式。此條款的主要內容就是告訴你該如何去寫你要的swap函式,下面開始正文來好好地介紹一下此條款的內容 1.首先來看一下庫裡面給的swap函式的原型 可以看出來標準程式庫提供的swa...

Item 25 考慮實現乙個不拋異常的swap

swap 函式最初由 stl 引入,已經成為異常安全程式設計的關鍵函式,同時也是解決自賦值問題的通用機制。std 中它的基本實現是很直觀的 namespace std 可以看到,上述 swap 是通過賦值和拷貝構造實現的。所以 std swap 並未提供異常安全,但由於 swap 操作的重要性,我們...