1. 程式人生 > 其它 >Effective C++條款25:設計與宣告——考慮寫出一個不拋異常的swap函式

Effective C++條款25:設計與宣告——考慮寫出一個不拋異常的swap函式

技術標籤:effective c++c++

一、標準模板庫中的swap函式

  • 標準模板庫中的swap函式,其是STL中的一部分,後來成為異常安全性程式設計(見條款29)以及用來處理自我賦值可能性(見條款11)的一個常見機制
  • 下面是STL中swap的原始碼:只要型別T支援拷貝(拷貝建構函式或者拷貝賦值運算子),預設的swap函式就可能幫你對兩個型別為T的物件進行兌換

  • 為什麼不使用std::swap()函式:
    • 從原始碼可以看出,std::swap函式,其涉及三個物件的複製操作:a複製到temp,b複製到a,temp複製到b
    • 但是對於我們自己設計的類來說,可能有時用不到這些複製操作,複製太多,效率太低
  • 因此對於自己設計的類(非模板類,或模板類),都不想使用std提供的預設swap()版本。本文下面將會介紹針對於自己設計的“非模板類,或模板類”設計自己的swap()函式

二、針對於非模板類,設計全特化的std::swap()

①有些情況下我們不希望使用std::swap()

  • 例如我們有下面的一個類WidgetImpl,其中儲存Widget的資料:
//針對Widget資料設計的class
class WidgetImpl
{
public:
    //...
private:
    int a, b, c;
    std::vector<double> v;
    //...其他資料
};
  • 此時又有一個類Widget,其採用pimpl手法(參閱條款31),裡面儲存一個指標指向於一個物件(此處為WidgetImpl),該物件內含真正的資料:
//這個class使用pimpl手法
class Widget
{
public:
    Widget(const Widget& rhs);
    Widget& operator=(const Widget& rhs)//拷貝賦值運算子
    {
        //...
        *pImpl = *(rhs.pImpl);
        //...
    }
private:
    WidgetImpl* pImpl; //此處是指標
};

為什麼我們不希望使用std::swap():

  • 我們使用時真正用到的是Widget物件,如果我們使用std::swap函式,那麼其會進行3次Widget物件的複製,並且每次複製的時候呼叫的是上面Widget類中的operator=運算子
  • 因此std::swap函式會呼叫三次operator=函式,這種效率是非常低的
  • 可以看出Widget中只用一個pImpl指標儲存資料,因此我們想要交換兩個Widget物件,只要交換它們兩個的pImpl指標就行了,這樣就達到資料互換的目的了(實現見下面的②)

②使用全特化的std::swap()函式

  • 我們希望讓std::swap()知道,當Widget類進行替換的時候只需要替換其內部的pImpl指標就可以了,此時我們可以針對std::swap()做一個全特化的版本
  • 程式碼如下:
//此swap用於表示這個函式針對“T是Widget”而設計的
namespace std {
    //template<>用於表示這是一個std::Swap的全特化版本
    template<>
    void swap<Widget>(Widget& a, Widget& b)
    {
        //錯誤的,pImpl是private的,無法編譯通過
        swap(a.pImpl, b.pImpl);
    }
}
  • 但是上面的程式碼是無法編譯通過的,因為pImpl是private的,因此函式無法編譯通過

③為類設計一個swap成員函式,並設計一個全域性swap函式

  • 通過②的設計我們知道,由於pImpl是private的,因此全特化的swap無法編譯通過。我們有兩種解決辦法:
    • 1.將上面的特化版本定義為class的friend,但是我們不希望這麼做
    • 2.(此處要介紹的)為class設計一個swap()函式,在全特化的版本中呼叫我們class的swap()成員函式
  • 此時我們可以修改Widget class,使全特化的版本可以應用於我們的Widgte class:
class Widget
{
public:
    void swap(Widget& rhs)
    {
        using std::swap; //為什麼要有這個宣告,見下
        swap(pImpl, rhs.pImpl); //呼叫std::swap()函式,只交換兩個物件的指標
    }
private:
    WidgetImpl* pImpl;
};


namespace std {
template<>
    void swap<Widget>(Widget& a, Widget& b)
    {
        a.swap(b); //呼叫Widget::swap()成員函式
    }
}
  • 現在的程式碼可以編譯通過了,並且可以使用我們的std::swap()全特化版本了
  • STL容器中的swap()成員函式就是按照這種原理實現的:為class提供public swap()成員函式和std::swap()的全特化版本(然後以後者呼叫前者)
  • 順便提一下:上面我們是在std中偏特化了std::swap()函式,但是我們不建議這樣做,因為這樣可能會對std名稱空間造成汙染,一種解決解決方法是在std名稱空間之外定義全特化swap()函式(下面的“三”中有演示案例)

三、如果是模板類,那麼該如何解決?

  • 緊接著“二”,如果Widget和WidgetImpl都不是普通的類,而是模板類。如下所示:
//此時這兩個類都變成模板類

template<typename T>
class WidgetImpl { };

template<typename T>
class Widget { };

偏特化是錯誤的

  • 對於“二”中的解法,我們可能會設計下面的程式碼(但是是錯誤的,無法編譯通過):
    • 我們的Widget和WidgetImpl都是類模板,因此我們嘗試偏特化std::swap()函式
    • 但是由於C++只允許對類模板偏特化,不允許對函式模板偏特化,所以下面的程式碼是無法編譯通過的
template<typename T>
class WidgetImpl{ //同上 };

template<typename T>
class Widget{ //同上 };

namespace std {
    //此處是錯誤的,C++只允許對類模板偏特化,不允許對函式模板偏特化
    template<typename T>
    void swap<Widget<T>>(Widget<T>& a, Widget<T>& b)
    {
        a.swap(b);
    }
}

使用過載版本代替偏特化

  • 上面我們嘗試偏特化函式模板,但是C++編譯器不允許。因此為了解決這種問題,我們可以為其新增一個過載版本
  • 程式碼如下:
template<typename T>
class WidgetImpl{ //同上 };

template<typename T>
class Widget{ //同上 };

namespace std{
    //這裡std::swap的一個過載版本,而不是特化版本
    template<typename T>
    void swap(Widget<T>& a, Widget<T>& b)
    {
        a.swap(b);
    }
}

在自己的名稱空間中定義swap()函式

  • 上面我們過載了std::swap函式,但是是在std名稱空間中進行過載的,std是個特殊的名稱空間,其管理規則比較特殊,因此我們不建議在std中過載任何內容
  • 一種解決方法就是在自己的名稱空間中定義swap()函式。程式碼如下:
//假設這是自己設計的名稱空間
namespace WidgetStuff{
template<typename T>
class WidgetImpl{ //同上 };
template<typename T>
class Widget{ //同上 };

template<typename T>
    void swap(Widget<T>& a, Widget<T>& b)
    {
        a.swap(b);
    }
}
  • 注意事項:
    • 我們在自己的名稱空間中定義的swap()函式不屬於std::swap()的過載版本,因為它們作用域不一致
    • 此處我們以名稱空間為例,其實不使用名稱空間也可以,我們主要是為了指出不要與std::swap()產生衝突。但是為了讓全域性資料空間太過雜亂,我們建議使用名稱空間
    • 在“二”中最後我們提到過,不要在std中進行全特化。因此此處的方法不僅適用於模板類,同樣也適用於非模板類

四、使用using宣告

  • 假設此時我們編寫了一個函式模板,其接受兩個引數,並在模板內調換兩個元素。程式碼如下:
template<typename T>
void doSomething(T& obj1,T& obj2)
{
    //...
    swap(obj1,obj2); //呼叫哪一個版本的swap()函式哪?
    //...
}
  • 根據C++的名稱查詢規則,對於swap()的呼叫會根據以下順序查詢:
    • 先在doSomething()函式所在的名稱空間中,查詢是否有針對於型別T所特化的swap()函式。如果有就呼叫,如果沒有進行下一步
    • 呼叫std::swap()函式調換兩個元素
  • 通過上面我們知道:
    • 在呼叫swap()時,先在自己的名稱空間中查詢是否有適當的特化版本的swap()函式,如果有就呼叫,如果沒有就呼叫std::swap()
    • 因此,為了使std::swap()函式在函式內可用,我們通常使用using宣告,匯入std::swap()函式。讓其在沒有特化版本的swap()時去呼叫std::swap()
template<typename T>
void doSomething(T& obj1,T& obj2)
{
    //使std::swap在次函式內可用
    //如果沒有針對於T的特化swap版本,那麼就呼叫std::swap
    using std::swap;

    //...
    swap(obj1,obj2);
    //...
}
  • 一個注意事項:
    • 不要在呼叫swap()函式的時候指定std::限定符,否則將永遠無法呼叫自己的特化版本
    • 例如下面的程式碼:強制呼叫std::swap(),那麼自己定義的特化swap()將永遠不會被呼叫
template<typename T>
void doSomething(T& obj1,T& obj2)
{
    //...
    std::swap(obj1,obj2); //錯誤的swap呼叫方式
    //...
}

五、swap()函式的總結

1)如果std::swap()函式對你的類或類模板使用,並且不會影響效率,那麼就優先使用std::swap()

2)如果std::swap()針對於你的類或類模板不太使用,那麼就自己設計swap()函式。設計如下:

  • 在類中提供一個public swap()函式,在其中置換兩者的資料(程式碼自己設計,但是不允許丟擲異常)
  • 然後在你的類或類模板所在的名稱空間中提供一個非成員函式swap(),然後在其中呼叫成員函式swap()
  • 如果你的類(而不是類模板),那麼可以為你的類特化std::swap(),並在特化版本的swap()中呼叫成員函式swap()

六、swap成員函式不能丟擲異常

  • 成員版的swap函式絕不可能丟擲異常。因為swap的一個最好的應用是幫助類(和類模板)提供強烈的異常安全性保障(條款29介紹)
  • 但此技術基於一個假設:成員版的swap絕不丟擲異常。這一約束只施加於swap成員版本,不可施加於非成員版本,因為swap預設版本是以拷貝建構函式和拷貝賦值運算子為基礎,而一般情況下兩者都允許丟擲異常
  • 因此當你寫下一個自定義的swap,往往提供的不只是高效置換物件的版本,而且還不丟擲異常

七、本文總結

  • 當std::swap對你的型別效率不高時,提供一個swap成員函式,並確定這個函式不丟擲異常
  • 如果你提供一個member swap,也該提供一個non-member swap用來呼叫前者。對於class(而非template class),也請特化std::swap
  • 呼叫swap時應針對std::swap使用using宣告式,然後呼叫swap並且不帶任何“名稱空間資格修飾”
  • 用“使用者定義型別”進行std template全特化時最好的,但千萬不要嘗試在std內加入某些對std而言全新的東西