[轉貼] 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 會是 true(ifstream 的確繼承自 istream),IsDerivedFrom<int, double>::value 則是 false(int 並非繼承自 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
請注意這裡只是個宣告,真正的定義只能在 Check 為 true 時才能出現,因此定義應該出現在針對 Check 進行 partial specialization 的地方(註):
template<typename T>
class Test<T, true> { // partial specialization
// class definition goes here
};
註:當然也可以把定義寫在 primary template,然後把 Check 為 false 的情況導到會產生編譯錯誤的地方。做法很多種。
因為 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 只是個宣告,又只針對 Check 為 true 提供定義,於是便形成編譯錯誤。
若要讓編譯訊息較為好懂一些,以下是個可能的做法:
template<typename T>
class Test<T, false> { // partial specialization for Check == false
Test Test_template_argument_constraints_check_failed;
};
我們也針對 Check 為 false 的狀況提供特化,裡面是個顯然有問題的變數,名稱是我們想顯示的訊息。因為未具現化的 template 不會進行 type-checking(此部份尚待查證),因此若未牴觸 template argument 的限制,就不會造成編譯錯誤。然而若有牴觸,編譯器就會嘗試具現化這個有問題的定義,進而產生編譯錯誤,錯誤訊息內會有個變數名稱,也就讓 programmer 了解到出了什麼問題。
事實上寫這篇的動機是看到 Java SDK 1.5 新支援的泛型,其中可以直接指定 generic parameter 必須 extend 什麼類別,於是心血來潮也用 C++ template 做類似的東西。
--
原文發表時間 2005/07/13 Wed 17:21:49。


<< 回到主頁