通用演算法常對處理對象施加特定限制。例如 std::sort() 需要元素定義 < 運算子。強制此約束很容易:編譯器嘗試針對給定型別呼叫該運算子,若不存在即產生編譯錯誤:
#include <algorithm>
struct S {}; // 未定義 operator<
int main()
{
S s[2];
std::sort(s, s+2); // 錯誤:型別 'S' 中未實作 'operator<'
}
但表達與強制執行抽象約束(如「必須是基類別」或「必須是 POD 型別」)並非易事,需要程式設計師更多彈性與技巧。本文示範如何以通用方式實現此類約束。
假設應用程式需要介面與非 C++ 模組(C 或 SQL 撰寫)互動,需保證傳遞給非 C++ 模組的所有物件具備 POD 型別(Plain Old Data,純舊資料型別)。
以下為 POD 型別:
struct S1 { int x; };
class S2 { public: void func(); };
union S3 { struct { int x, y; } t; char c[4]; };
struct S4 : S1, S2 {};
以下非 POD 型別:
struct C1 { virtual void func(); }; // 有虛擬函式
struct C2 { struct T{ int x, y; }; ~C2(); }; // 有解構函式
struct C3 : virtual S1 {}; // 有虛擬基類別
在個別程式實作中 POD 與非 POD 使用嚴格區分。<cstddef> 中標準巨集 offsetof() 即為例:
size_t nbytes = offsetof(S, mem);
此運算式以位元組為單位傳回成員 mem 的位移量。依 C++ 標準,S 必須是 POD 類別、結構或聯合,否則結果未定義。任務是撰寫約束,能在編譯期自動區分 POD 與非 POD 型別,一旦違反「必須是 POD 型別」約束,編譯器發出明確錯誤訊息。
約束實際上是類別模板成員函式中的一個運算式或宣告。違反約束時,上述運算式觸發編譯錯誤。關鍵在於找到正確的編譯期運算式,在約束違反時啟動。熟悉 C++ 標準當然有益。標準規定非 POD 物件不能是聯合 (union) 成員(見標準 clause 9.5)。利用此限制,建立一個聯合,讓其唯一成員即為要測試的物件:
template <class T> struct POD_test
{
POD_test()
{
union { T t; } u; // T 必須是 POD 型別
}
};
編譯器僅為實際被呼叫的成員函式產生程式碼(顯式或隱式)。因此在類別模板建構函式或解構函式實作約束,可保證編譯期處理每個實例。
測試程式碼:
// 以下三條通過編譯
POD_test<int> pi;
POD_test<S1> ps1;
POD_test<S4> ps4;
// 編譯失敗
POD_test<std::string> pstr;
POD_test<C1> pc1;
POD_test<C2> pc2;
後三個實例因模板參數非 POD 物件,編譯器發出錯誤訊息,符合預期。
約束會導致執行期與空間開銷嗎?若在編譯期檢查,不想讓它留在可執行檔中。現代 IDE 很聰明,會優化掉可執行檔中不必要程式碼。為讓編譯器報錯,將約束移至獨立靜態成員函式 constraints()(函式名可自訂),宣告為 private 避免其他程式呼叫:
template <class T> struct POD_test
{
POD_test() { constraints(); } // 強制編譯期處理
private:
static void constraints()
{
union { T t; } u;
}
};
constraints() 實際上什麼都不做,僅宣告一個區域聯合型別變數。因該聯合變數未被使用,編譯器可省略建構函式中的 constraints(),避免不必要開銷。
「必須是 T2 型別」約束:
// 檢查 T1 是否為 T2
template <class T1, class T2> struct is_a_T2
{
is_a_T2() { constraints(); }
static void constraints()
{
T1 t1;
T2& ref = t1; // 若 t1 非 t2 則出錯
}
};
「必須是整數型別」約束:
有多種技術途徑實現。其中之一是使用該物件作為陣列下標。因 C++ 要求整數型別作為下標,使用其他型別將導致編譯錯誤。實作留給讀者練習。
Danny Kalev 為通過認證的系統分析師,專攻 C++ 與形式語言理論的軟體工程師。1997-2000 年間為 C++ 標準委員會成員。業餘喜好古典音樂、維多利亞時期文學、自然語言研究、考古與地理。常至 C++ 論壇並定期為 C++ 網站與雜誌撰稿,亦在教育機構講授程式設計語言與應用語言課程。