Effective C++筆記: 設計與聲明(四)

 

Item 25: 考慮寫出一個不拋出異常的 swap函數

標準程序庫中的swap行爲:

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。對一些類型來說,這些複製動作是沒有必要的。最主要的就是採用pimpl(pointer to implementation), “以指針指向一個對象,內含真正數據“的這種類型。

如:

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's data

爲了交換這兩個 Widget 對象的值,我們實際要做的就是交換它們的 pImpl 指針,但是缺省的交換算法沒有辦法知道這些。它不僅要拷貝三個 Widgets,而且還有三個 WidgetImpl 對象,效率太低了。

 

當交換 Widgets 的是時候,我們應該告訴 std::swap 我們打算做什麼,執行交換的方法就是交換它們內部的 pImpl 指針。這種方法的正規說法是:針對 Widget 特化 std::swapspecialize 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
}

可是,就像我說的,這個函數還不能編譯。那是因爲它試圖訪問 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 的特化來調用這些成員函數。

 

stlvectorswap實現:(SGI STL 3.3)

std::swap:

template <class _Tp>

inline void swap(_Tp& __a, _Tp& __b) {

  __STL_REQUIRES(_Tp, _Assignable);

  _Tp __tmp = __a;

  __a = __b;

  __b = __tmp;

}

 

vectorpublic swap(其實就是交換三個_Tp* 指針既可)

  void swap(vector<_Tp, _Alloc>& __x) {

    __STD::swap(_M_start, __x._M_start);

    __STD::swap(_M_finish, __x._M_finish);

    __STD::swap(_M_end_of_storage, __x._M_end_of_storage);

  }

 

特化swap(調用vector類的public  swap成員)

template <class _Tp, class _Alloc>

inline void swap(vector<_Tp, _Alloc>& __x, vector<_Tp, _Alloc>& __y)

{

  __x.swap(__y);

}

 

!!給自己的模板類添加swap:

假設 Widget WidgetImpl 是類模板,而不是類,或許因此我們可以參數化存儲在 WidgetImpl 中的數據類型:(其實這就和stl vector的行爲一樣)

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),但不允許函數模板這樣做。這樣的代碼不能編譯.

當我們想要部分特化一個函數模板時,通常做法是簡單地增加一個重載。看起來就像這樣:(pure like std::vector的實現辦法)

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,只是不再將那個非成員函數聲明爲 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);
  }
}

然後,假設你寫了這樣一個函數模板來交換兩個對象的值:

template<typename T>
void doSomething(T& obj1, T& obj2)
{
  ...
  swap(obj1, obj2);
  ...
}

當編譯器看到這個 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 進行了特化,則特化的版本會被使用。

採用如下的版本可以保證以上的調用順序:

如果 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 的缺省實現效率不足(這幾乎總是意味着你的類或模板使用了某種 pimpl idiom 的變種),就按照以下步驟來做:

1.   提供一個能高效地交換你的類型的兩個對象的值的 public swap 成員函數。出於我過一會兒就要解釋的動機,這個函數應該永遠不會拋出異常

2.   在你的類或模板所在的同一個 namespace 中提供一個非成員的 swap。用它調用你的 swap 成員函數。

3.   如果你寫了一個類(不是類模板),就爲你的類特化 std::swap。用它也調用你的 swap 成員函數。

最後,如果你調用 swap,請確保在你的函數中包含一個 using declaration 使 std::swap 可見,然後在調用 swap 時不使用任何 namespace 限定條件。

 

成員版swap絕不能拋出異常!!(後繼條款29對此說明)

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章