[轉貼] 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。
<< 回到主頁