2006/08/10

[轉貼] Static C++ Template Argument Constraints Checking

去年暑假貼在彰中華陽夢想家的文章,和下面那一篇使用相同手法,轉貼於此。


話說 C++ TR1 裡包含了一些令人熱血沸騰的東西,其中一個是所謂的 type traits,可用以萃取型別的一些特性,譬如說是不是 POD、A 型別是不是衍生自 B 型別之類。基本上,就是 Boost Type Traits Library 的那些東西吧。這些 traits 的判斷動作都是在編譯期執行(應該吧 XD),所以判斷結果的值(true or false)都是constant expression,可以在編譯期使用。

以下是一段 Andrei Alexandrescu 設計的 IsDerivedFrom template,此 template 有兩個 type parameters,可判斷第一個型別是否衍生自第二個型別。

既然是 Alexandrescu 的手筆,沒看過的恐怕會有點不適應 :) :

template<typename D, typename B>
struct IsDerivedFrom {
  class Yes_ {};
  class No_ {
    Yes_ a_[2];
  };
  static Yes_ check_(B*);
  static No_  check_(...);
  enum{value = sizeof(check_(static_cast<D*>(0))) == sizeof(Yes_)};
};

使用這個 template 的方法很簡單,例如 IsDerivedFrom<ifstream, istream>::value 會是 trueifstream 的確繼承自 istream),IsDerivedFrom<int, double>::value 則是 falseint 並非繼承自 double)。至於實作,就牽扯到一些語言細節。

首先,IsDerivedFrom struct 裡定義了兩個 classes:Yes_No_。其中 Yes_ 雖然內部沒有任何資料,但仍保證會得到儲存空間。而 No_ 裡面配置了大小為 2 的 Yes_ 陣列,使得 sizeof(No_) != sizeof(Yes_)

接著是最下面的 enumeration,定義了一個 enumerator 稱為 value,這是個常見的 class 內常數定義手法。這個常數以一個看起來有點複雜的算式初始化,判斷兩個東西的大小是否相同。Equality operator 的右側沒有問題,至於左側喚起了 static member function check_(),有兩個 overloaded 版本。其一接收 B 型別的指標,另一個是省略符號。當 equality operator 左側的 sizeof 算式被求值時,並不會真正喚起 check_(因此不需要提供函式定義),但仍然要依據 check_ 獲得的引數進行多載決議程序。這個引數是一個 D 型別指標,從 0(null)轉型而來,如果 D 繼承自 B,那麼 D* 就可以自動轉換至 B*,決議結果就會是傳回 Yes_check_,於是等號成立,讓 value 被初始化為 true。然而如果 D* 無法轉換至 B*(亦即 D 並非衍生自 B),那麼這個呼叫動作便無法匹配 check_(B*),所幸還有另一個 check_,接收的參數是最寬鬆的「...」,於是會決議為該函式,傳回值為 No_。最後因為兩邊的 sizeof() 結果不等,value 所獲得的初值就是 false

上面長長的一大段,看起來有點嚇人。幸運的是,身為 library user,我們並不需要煩惱實作細節。類似的 templates 可由 Boost Type Traits Library 獲得,甚至幾乎可確定下一版的 C++ Standard 會把這類 templates 納入 standard library 之中。

顯然還沒切入我們的主題「static template argument constraints checking」。有了上面的 template,我們可以搭配 class template partial specialization 完成條件限制的檢查。假設有個 Test class 的 T parameter 必須衍生自 std::istream

template<typename T> // T must derive from std::istream
class Test {
  // ...
};

我們可以在 T 後面加上一個檢查用的 non-type template parameter,並給它一個初值

template<typename T, bool Check = IsDerivedFrom<T, std::istream>::value>
class Test; // Note: only declaration

請注意這裡只是個宣告,真正的定義只能在 Checktrue 時才能出現,因此定義應該出現在針對 Check 進行 partial specialization 的地方(註):

template<typename T>
class Test<T, true> { // partial specialization
  // class definition goes here
};

註:當然也可以把定義寫在 primary template,然後把 Checkfalse 的情況導到會產生編譯錯誤的地方。做法很多種。

因為 Check 有預設引數,我們可以依平常的習慣來使用 Test

Test<std::ifstream> t; // suppose there exists a default ctor for Test

而當我們寫

Test<int> t; // error: int does not derive from std::istream

編譯器會企圖找到 Test<int, false> 的定義,但 primary template 只是個宣告,又只針對 Checktrue 提供定義,於是便形成編譯錯誤。

若要讓編譯訊息較為好懂一些,以下是個可能的做法:

template<typename T>
class Test<T, false> { // partial specialization for Check == false
  Test Test_template_argument_constraints_check_failed;
};

我們也針對 Checkfalse 的狀況提供特化,裡面是個顯然有問題的變數,名稱是我們想顯示的訊息。因為未具現化的 template 不會進行 type-checking(此部份尚待查證),因此若未牴觸 template argument 的限制,就不會造成編譯錯誤。然而若有牴觸,編譯器就會嘗試具現化這個有問題的定義,進而產生編譯錯誤,錯誤訊息內會有個變數名稱,也就讓 programmer 了解到出了什麼問題。

事實上寫這篇的動機是看到 Java SDK 1.5 新支援的泛型,其中可以直接指定 generic parameter 必須 extend 什麼類別,於是心血來潮也用 C++ template 做類似的東西。

--
原文發表時間 2005/07/13 Wed 17:21:49。