2006/08/08

HasSwap

睡前讀先前評為「走火入魔」的《C++ Template Metaprogramming》,讀沒幾頁就想試寫「判斷一個 class type 是否具有 swap member function」的 type-trait class。成果如下(在 g++ 3.4.2/4.1.0 & Visual C++ 2005 Express Ed. 之下測試成功)。

 1: template <typename T>
 2: class HasSwap {
 3:   class Yes { };
 4:   class No  { Yes a[2]; };
 5: 
 6:   template <void (T::*)(T&)>  // signature of member swap
 7:   struct Wrapper {
 8:     Wrapper(int) { }
 9:   };
10: 
11:   template <typename C> static Yes check(Wrapper<&C::swap>);
12:   template <typename C> static No  check(...);
13: 
14: public:
15: 
16:   enum { value = sizeof(check<T>(0)) == sizeof(Yes) };
17: };

這個 trait class 仍採用奠基於 SFINAE(Substitution Failure Is Not An Error)原則(ref.《C++ Templates ─ the Complete Guide》p.106)的標準手法:先定義兩個大小不同的 types(L3~4)作為兩個同名函式(L11~12)的 return types,然後(L16)引發函式重載決議(overload resolution)機制,並用 sizeof 判斷選用的是哪個函式。(ref.《Modern C++ Design》Sec. 2.7, p.34)

此處值得注意的是「避免無效算式爆炸」的技巧,即當 &T::swap 為無效算式時如何避免編譯器報錯。SFINAE 原則是在 function templates 具現化失敗時生效,而不會只因為「可能核算無效算式」就生效(ref.《C++ Templates ─ the Complete Guide》p.107)。因此我們必須讓編譯器遇見無效算式時同時令 SFINAE 生效(否則編譯器會因為遇見無效算式而報錯),即無效算式必須使函式無法被具現化。作法是將 &T::swap 包覆於一個 class template 之內(即 L6~9 的 class Wrapper),從一個算式轉為型別。這個型別便可當作函式參數型別使用,而與重載決議機制掛鉤,進而能在那個「被包覆於內的算式」無效時觸發 SFINAE(因為無法具現化函式參數型別)。最後為了在 SFINAE 有機會介入時才試著核算那個算式,兩個 check 都是 function templates,不會在 HasSwap 被具現化時直接被具現化(因為 L11 的 check 可能不得具現化)而引爆無效算式。

有了這個 trait class,std::swap 便可改寫如下(假設可改寫):

namespace std {
  template <bool>  // value-to-type technique again
  struct __Bool2Type { };

  template <typename T>
  void __swap(T& a, T& b, __Bool2Type<true>) {
    a.swap(b);
  }

  template <typename T>
  void __swap (T& a, T& b, __Bool2Type<false>) {
    T t = a;
    a = b;
    b = t;
  }

  template <typename T>
  void swap(T& a, T& b) {
    __swap(a, b, __Bool2Type<HasSwap<T>::value>());
  }   
}

如此一來,只要 T 具有 member function swap,對 std::swap 的呼叫就會自動轉發給那個 member swap,不需再為 T 手動特化(specialize)std::swap

這個 HasSwap 仍可再一般化為檢測任意 member 存在與否,但我不知道能不能避開 macro 而寫出不冗贅的使用介面(或者該問介面應如何設計)。即使維持原樣固定檢測 swap 的有無,這個 class 也有一個地方稍有侷限:swap 的 signature 被寫死為 void (T::*)(T&)。只要 signature 稍有不同,HasSwap<T>::value 就會回返 false。而《Exceptional C++ Style》p.29 如是說:

The standard library specification deliberately gives some leeway to implementers when it comes to member functions. Specifically:

  • A member function signature with default parameters might be replaced by "two or more member function signatures with the equivalent behavior".
  • A member function signature might have additional defaulted parameters.

... Because the signatures of standard library member functions are impossible to know exactly ─ unless you peek in your library implementation's header files to look for any peekaboo parameters(JK 注:即 library 實作者被允許任意加入的額外參數), and even then the answer might change on a new release of the same library ─ the bottom line is that you can't reliably form pointers to standard library member functions and still have portable code.

因此嚴格說起來,HasSwap 對於 standard library classes(e.g. vector<T>list<T>)並不可攜(not portable)。不過此處影響應該不太大,正如《Exceptional C++ Style》所說:

In practice, though, the problem might not be all that bad. I don't know whether library implementers widely avail themselves of the leeway to add extra parameters, or intend to do so in the future. To the extent that they don't do so, you won't encounter these difficulties in practice.

--
Type traits 基本上可算是 compile-time reflection 吧。

Blogger yen38/09/2006 5:54 pm 說:

同一句...這一次真的要很努力才能看懂了..XD

 

<< 回到主頁