實驗平台:Win7,VS2013 Community,GCC 4.8.3(在線版)
所謂元編程就是編寫直接生成或操縱程序的程序,C++ 模板給 C++ 語言提供了元編程的能力,模板使 C++ 編程變得異常靈活,能實現很多高級動態語言才有的特性(語法上可能比較丑陋,一些歷史原因見下文)。普通用戶對 C++ 模板的使用可能不是很頻繁,大致限於泛型編程,但一些系統級的代碼,尤其是對通用性、性能要求極高的基礎庫(如 STL、Boost)幾乎不可避免的都大量地使用 C++ 模板,一個稍有規模的大量使用模板的程序,不可避免的要涉及元編程(如類型計算)。本文就是要剖析 C++ 模板元編程的機制。
下面所給的所有代碼,想做實驗又懶得打開編譯工具?一個在線運行 C++ 代碼的網站(GCC 4.8)很好~
(本博文地址:http://www.cnblogs.com/liangliangh/p/4219879.html,轉載版本將得不到作者維護)
1. C++模板的語法
函數模板(function template)和類模板(class template)的簡單示例如下:
#include <iostream> // 函數模板 template<typename T> bool equivalent(const T& a, const T& b){ return !(a < b) && !(b < a); } // 類模板 template<typename T=int> // 默認參數 class bignumber{ T _v; public: bignumber(T a) : _v(a) { } inline bool operator<(const bignumber& b) const; // 等價於 (const bignumber<T> b) }; // 在類模板外實現成員函數 template<typename T> bool bignumber<T>::operator<(const bignumber& b) const{ return _v < b._v; } int main() { bignumber<> a(1), b(1); // 使用默認參數,"<>"不能省略 std::cout << equivalent(a, b) << '\n'; // 函數模板參數自動推導 std::cout << equivalent<double>(1, 2) << '\n'; std::cin.get(); return 0; }
程序輸出如下:
1 0
關於模板(函數模板、類模板)的模板參數(詳見文獻[1]第3章):
模板特例化(template specialization,又稱特例、特化)的簡單示例如下:
// 實現一個向量類 template<typename T, int N> class Vec{ T _v[N]; // ... // 模板通例(primary template),具體實現 }; template<> class Vec<float, 4>{ float _v[4]; // ... // 對 Vec<float, 4> 進行專門實現,如利用向量指令進行加速 }; template<int N> class Vec<bool, N>{ char _v[(N+sizeof(char)-1)/sizeof(char)]; // ... // 對 Vec<bool, N> 進行專門實現,如用一個比特位表示一個bool };
所謂模板特例化即對於通例中的某種或某些情況做單獨專門實現,最簡單的情況是對每個模板參數指定一個具體值,這成為完全特例化(full specialization),另外,可以限制模板參數在一個范圍取值或滿足一定關系等,這稱為部分特例化(partial specialization),用數學上集合的概念,通例模板參數所有可取的值組合構成全集U,完全特例化對U中某個元素進行專門定義,部分特例化對U的某個真子集進行專門定義。
更多模板特例化的例子如下(參考了文獻[1]第44頁):
template<typename T, int i> class cp00; // 用於模板型模板參數 // 通例 template<typename T1, typename T2, int i, template<typename, int> class CP> class TMP; // 完全特例化 template<> class TMP<int, float, 2, cp00>; // 第一個參數有const修飾 template<typename T1, typename T2, int i, template<typename, int> class CP> class TMP<const T1, T2, i, CP>; // 第一二個參數為cp00的實例且滿足一定關系,第四個參數為cp00 template<typename T, int i> class TMP<cp00<T, i>, cp00<T, i+10>, i, cp00>; // 編譯錯誤!,第四個參數類型和通例類型不一致 //template<template<int i> CP> //class TMP<int, float, 10, CP>;
關於模板特例化(詳見文獻[1]第4章):
對模板的多個實例,類型等價(type equivalence)判斷規則(詳見文獻[2] 13.2.4):同一個模板(模板名及其參數類型列表構成的模板簽名(template signature)相同,函數模板可以重載,類模板不存在重載)且指定的模板實參等價(類型參數是等價類型,非類型參數值相同)。如下例子:
#include <iostream> // 識別兩個類型是否相同,提前進入模板元編程^_^ template<typename T1, typename T2> // 通例,返回 false class theSameType { public: enum { ret = false }; }; template<typename T> // 特例,兩類型相同時返回 true class theSameType<T, T> { public: enum { ret = true }; }; template<typename T, int i> class aTMP { }; int main(){ typedef unsigned int uint; // typedef 定義類型別名而不是引入新類型 typedef uint uint2; std::cout << theSameType<unsigned, uint2>::ret << '\n'; // 感謝 C++11,連續角括號“>>”不會被當做流輸入符號而編譯錯誤 std::cout << theSameType<aTMP<unsigned, 2>, aTMP<uint2, 2>>::ret << '\n'; std::cout << theSameType<aTMP<int, 2>, aTMP<int, 3>>::ret << '\n'; std::cin.get(); return 0; }
1 1 0
關於模板實例化(template instantiation)(詳見文獻[4]模板):
隱式實例化時,成員只有被引用到才會進行實例化,這被稱為推遲實例化(lazy instantiation),由此可能帶來的問題如下面的例子(文獻[6],文獻[7]):
#include <iostream> template<typename T> class aTMP { public: void f1() { std::cout << "f1()\n"; } void f2() { std::ccccout << "f2()\n"; } // 敲錯鍵盤了,語義錯誤:沒有 std::ccccout }; int main(){ aTMP<int> a; a.f1(); // a.f2(); // 這句代碼被注釋時,aTMP<int>::f2() 不被實例化,從而上面的錯誤被掩蓋! std::cin.get(); return 0; }
所以模板代碼寫完後最好寫個諸如顯示實例化的測試代碼,更深入一些,可以插入一些模板調用代碼使得編譯器及時發現錯誤,而不至於報出無限長的錯誤信息。另一個例子如下(GCC 4.8 下編譯的輸出信息,VS2013 編譯輸出了 500 多行錯誤信息):
#include <iostream> // 計算 N 的階乘 N! template<int N> class aTMP{ public: enum { ret = N==0 ? 1 : N * aTMP<N-1>::ret }; // Lazy Instantiation,將產生無限遞歸! }; int main(){ std::cout << aTMP<10>::ret << '\n'; std::cin.get(); return 0; }
sh-4.2# g++ -std=c++11 -o main *.cpp main.cpp:7:28: error: template instantiation depth exceeds maximum of 900 (use -ftemplate-depth= to increase the maximum) instantiating 'class aTMP<-890>' enum { ret = N==0 ? 1 : N * aTMP<N-1>::ret }; ^ main.cpp:7:28: recursively required from 'class aTMP<9>' main.cpp:7:28: required from 'class aTMP<10>' main.cpp:11:23: required from here main.cpp:7:28: error: incomplete type 'aTMP<-890>' used in nested name specifier
上面的錯誤是因為,當編譯 aTMP<N> 時,並不判斷 N==0,而僅僅知道其依賴 aTMP<N-1>(lazy instantiation),從而產生無限遞歸,糾正方法是使用模板特例化,如下:
#include <iostream> // 計算 N 的階乘 N! template<int N> class aTMP{ public: enum { ret = N * aTMP<N-1>::ret }; }; template<> class aTMP<0>{ public: enum { ret = 1 }; }; int main(){ std::cout << aTMP<10>::ret << '\n'; std::cin.get(); return 0; }
3228800
關於模板的編譯和鏈接(詳見文獻[1] 1.3、文獻[4]模板):
實例化,編譯鏈接的簡單例子如下(參考了文獻[1]第10頁):
// file: a.cpp #include <iostream> template<typename T> class MyClass { }; template MyClass<double>::MyClass(); // 顯示實例化構造函數 MyClass<double>::MyClass() template class MyClass<long>; // 顯示實例化整個類 MyClass<long> template<typename T> void print(T const& m) { std::cout << "a.cpp: " << m << '\n'; } void fa() { print(1); // print<int>,隱式實例化 print(0.1); // print<double> } void fb(); // fb() 在 b.cpp 中定義,此處聲明 int main(){ fa(); fb(); std::cin.get(); return 0; }
// file: b.cpp #include <iostream> template<typename T> void print(T const& m) { std::cout << "b.cpp: " << m << '\n'; } void fb() { print('2'); // print<char> print(0.1); // print<double> }
a.cpp: 1 a.cpp: 0.1 b.cpp: 2 a.cpp: 0.1
上例中,由於 a.cpp 和 b.cpp 中的 print<double> 實例等價(模板實例的二進制代碼在編譯生成的對象文件 a.obj、b.obj 中),故鏈接時消除了一個(消除哪個沒有規定,上面消除了 b.cpp 中的)。
關於 template、typename、this 關鍵字的使用(文獻[4]模板,文獻[5]):
一個例子如下(需要 GCC 編譯,GCC 對 C++11 幾乎全面支持,VS2013 此處總是在基類中查找名字,且函數模板前不需要 template):
#include <iostream> template<typename T> class aTMP{ public: typedef const T reType; }; void f() { std::cout << "global f()\n"; } template<typename T> class Base { public: template <int N = 99> void f() { std::cout << "member f(): " << N << '\n'; } }; template<typename T> class Derived : public Base<T> { public: typename T::reType m; // typename 不能省略 Derived(typename T::reType a) : m(a) { } void df1() { f(); } // 調用全局 f(),而非想象中的基類 f() void df2() { this->template f(); } // 基類 f<99>() void df3() { Base<T>::template f<22>(); } // 強制基類 f<22>() void df4() { ::f(); } // 強制全局 f() }; int main(){ Derived<aTMP<int>> a(10); a.df1(); a.df2(); a.df3(); a.df4(); std::cin.get(); return 0; }
global f() member f(): 99 member f(): 22 global f()
C++11 關於模板的新特性(詳見文獻[1]第15章,文獻[4]C++11):
在本文中,如無特別聲明將不使用 C++11 的特性(除了 “>>”)。
2. 模板元編程概述
如果對 C++ 模板不熟悉(光熟悉語法還不算熟悉),可以先跳過本節,往下看完例子再回來。
C++ 模板最初是為實現泛型編程設計的,但人們發現模板的能力遠遠不止於那些設計的功能。一個重要的理論結論就是:C++ 模板是圖靈完備的(Turing-complete),其證明過程請見文獻[8](就是用 C++ 模板模擬圖靈機),理論上說 C++ 模板可以執行任何計算任務,但實際上因為模板是編譯期計算,其能力受到具體編譯器實現的限制(如遞歸嵌套深度,C++11 要求至少 1024,C++98 要求至少 17)。C++ 模板元編程是“意外”功能,而不是設計的功能,這也是 C++ 模板元編程語法丑陋的根源。
C++ 模板是圖靈完備的,這使得 C++ 成為兩層次語言(two-level languages,中文暫且這麼翻譯,文獻[9]),其中,執行編譯計算的代碼稱為靜態代碼(static code),執行運行期計算的代碼稱為動態代碼(dynamic code),C++ 的靜態代碼由模板實現(預處理的宏也算是能進行部分靜態計算吧,也就是能進行部分元編程,稱為宏元編程,見 Boost 元編程庫即 BCCL,文獻[16]和文獻[1] 10.4)。
具體來說 C++ 模板可以做以下事情:編譯期數值計算、類型計算、代碼計算(如循環展開),其中數值計算實際不太有意義,而類型計算和代碼計算可以使得代碼更加通用,更加易用,性能更好(也更難閱讀,更難調試,有時也會有代碼膨脹問題)。編譯期計算在編譯過程中的位置請見下圖(取自文獻[10]),可以看到關鍵是模板的機制在編譯具體代碼(模板實例)前執行:
函數式編程(functional programming),它的主要特點是:函數調用不產生任何副作用(沒有可變的存儲),用遞歸形式實現循環結構的功能。C++ 模板的特例化提供了條件判斷能力,而模板遞歸嵌套提供了循環的能力,這兩點使得其具有和普通語言一樣通用的能力(圖靈完備性)。
從編程形式來看,模板的“<>”中的模板參數相當於函數調用的輸入參數,模板中的 typedef 或 static const 或 enum 定義函數返回值(類型或數值,數值僅支持整型,如果需要可以通過編碼計算浮點數),代碼計算是通過類型計算進而選擇類型的函數實現的(C++ 屬於靜態類型語言,編譯器對類型的操控能力很強)。代碼示意如下:
#include <iostream> template<typename T, int i=1> class someComputing { public: typedef volatile T* retType; // 類型計算 enum { retValume = i + someComputing<T, i-1>::retValume }; // 數值計算,遞歸 static void f() { std::cout << "someComputing: i=" << i << '\n'; } }; template<typename T> // 模板特例,遞歸終止條件 class someComputing<T, 0> { public: enum { retValume = 0 }; }; template<typename T> class codeComputing { public: static void f() { T::f(); } // 根據類型調用函數,代碼計算 }; int main(){ someComputing<int>::retType a=0; std::cout << sizeof(a) << '\n'; // 64-bit 程序指針 // VS2013 默認最大遞歸深度500,GCC4.8 默認最大遞歸深度900(-ftemplate-depth=n) std::cout << someComputing<int, 500>::retValume << '\n'; // 1+2+...+500 codeComputing<someComputing<int, 99>>::f(); std::cin.get(); return 0; }
8 125250 someComputing: i=99
C++ 模板元編程概覽框圖如下(取自文獻[9]):
3. 編譯期數值計算
第一個 C++ 模板元程序是 Erwin Unruh 在 1994 年寫的(文獻[14]),這個程序計算小於給定數 N 的全部素數(又叫質數),程序並不運行(都不能通過編譯),而是讓編譯器在錯誤信息中顯示結果(直觀展現了是編譯期計算結果,C++ 模板元編程不是設計的功能,更像是在戲弄編譯器,當然 C++11 有所改變),由於年代久遠,原來的程序用現在的編譯器已經不能編譯了,下面的代碼在原來程序基礎上稍作了修改(GCC 4.8 下使用 -fpermissvie,只顯示警告信息):
// Prime number computation by Erwin Unruh template<int i> struct D { D(void*); operator int(); }; // 構造函數參數為 void* 指針 template<int p, int i> struct is_prime { // 判斷 p 是否為素數,即 p 不能整除 2...p-1 enum { prim = (p%i) && is_prime<(i>2?p:0), i-1>::prim }; }; template<> struct is_prime<0, 0> { enum { prim = 1 }; }; template<> struct is_prime<0, 1> { enum { prim = 1 }; }; template<int i> struct Prime_print { Prime_print<i-1> a; enum { prim = is_prime<i, i-1>::prim }; // prim 為真時, prim?1:0 為 1,int 到 D<i> 轉換報錯;假時, 0 為 NULL 指針不報錯 void f() { D<i> d = prim?1:0; a.f(); } // 調用 a.f() 實例化 Prime_print<i-1>::f() }; template<> struct Prime_print<2> { // 特例,遞歸終止 enum { prim = 1 }; void f() { D<2> d = prim?1:0; } }; #ifndef LAST #define LAST 10 #endif int main() { Prime_print<LAST> a; a.f(); // 必須調用 a.f() 以實例化 Prime_print<LAST>::f() }
sh-4.2# g++ -std=c++11 -fpermissive -o main *.cpp main.cpp: In member function 'void Prime_print<2>::f()': main.cpp:17:33: warning: invalid conversion from 'int' to 'void*' [-fpermissive] void f() { D<2> d = prim ? 1 : 0; } ^ main.cpp:2:28: warning: initializing argument 1 of 'D<i>::D(void*) [with int i = 2]' [-fpermissive] template<int i> struct D { D(void*); operator int(); }; ^ main.cpp: In instantiation of 'void Prime_print<i>::f() [with int i = 7]': main.cpp:13:36: recursively required from 'void Prime_print<i>::f() [with int i = 9]' main.cpp:13:36: required from 'void Prime_print<i>::f() [with int i = 10]' main.cpp:25:27: required from here main.cpp:13:33: warning: invalid conversion from 'int' to 'void*' [-fpermissive] void f() { D<i> d = prim ? 1 : 0; a.f(); } ^ main.cpp:2:28: warning: initializing argument 1 of 'D<i>::D(void*) [with int i = 7]' [-fpermissive] template<int i> struct D { D(void*); operator int(); }; ^ main.cpp: In instantiation of 'void Prime_print<i>::f() [with int i = 5]': main.cpp:13:36: recursively required from 'void Prime_print<i>::f() [with int i = 9]' main.cpp:13:36: required from 'void Prime_print<i>::f() [with int i = 10]' main.cpp:25:27: required from here main.cpp:13:33: warning: invalid conversion from 'int' to 'void*' [-fpermissive] void f() { D<i> d = prim ? 1 : 0; a.f(); } ^ main.cpp:2:28: warning: initializing argument 1 of 'D<i>::D(void*) [with int i = 5]' [-fpermissive] template<int i> struct D { D(void*); operator int(); }; ^ main.cpp: In instantiation of 'void Prime_print<i>::f() [with int i = 3]': main.cpp:13:36: recursively required from 'void Prime_print<i>::f() [with int i = 9]' main.cpp:13:36: required from 'void Prime_print<i>::f() [with int i = 10]' main.cpp:25:27: required from here main.cpp:13:33: warning: invalid conversion from 'int' to 'void*' [-fpermissive] void f() { D<i> d = prim ? 1 : 0; a.f(); } ^ main.cpp:2:28: warning: initializing argument 1 of 'D<i>::D(void*) [with int i = 3]' [-fpermissive] template<int i> struct D { D(void*); operator int(); }; ^
上面的編譯輸出信息只給出了前一部分,雖然信息很雜,但還是可以看到其中有 10 以內全部素數:2、3、5、7(已經加粗顯示關鍵行)。
到目前為止,雖然已經看到了階乘、求和等遞歸數值計算,但都沒涉及原理,下面以求和為例講解 C++ 模板編譯期數值計算的原理:
#include <iostream> template<int N> class sumt{ public: static const int ret = sumt<N-1>::ret + N; }; template<> class sumt<0>{ public: static const int ret = 0; }; int main() { std::cout << sumt<5>::ret << '\n'; std::cin.get(); return 0; }
15
當編譯器遇到 sumt<5> 時,試圖實例化之,sumt<5> 引用了 sumt<5-1> 即 sumt<4>,試圖實例化 sumt<4>,以此類推,直到 sumt<0>,sumt<0> 匹配模板特例,sumt<0>::ret 為 0,sumt<1>::ret 為 sumt<0>::ret+1 為 1,以此類推,sumt<5>::ret 為 15。值得一提的是,雖然對用戶來說程序只是輸出了一個編譯期常量 sumt<5>::ret,但在背後,編譯器其實至少處理了 sumt<0> 到 sumt<5> 共 6 個類型。
從這個例子我們也可以窺探 C++ 模板元編程的函數式編程范型,對比結構化求和程序:for(i=0,sum=0; i<=N; ++i) sum+=i; 用逐步改變存儲(即變量 sum)的方式來對計算過程進行編程,模板元程序沒有可變的存儲(都是編譯期常量,是不可變的變量),要表達求和過程就要用很多個常量:sumt<0>::ret,sumt<1>::ret,...,sumt<5>::ret 。函數式編程看上去似乎效率低下(因為它和數學接近,而不是和硬件工作方式接近),但有自己的優勢:描述問題更加簡潔清晰(前提是熟悉這種方式),沒有可變的變量就沒有數據依賴,方便進行並行化。
4. 模板下的控制結構
模板實現的條件 if 和 while 語句如下(文獻[9]):
// 通例為空,若不匹配特例將報錯,很好的調試手段(這裡是 bool 就無所謂了) template<bool c, typename Then, typename Else> class IF_ { }; template<typename Then, typename Else> class IF_<true, Then, Else> { public: typedef Then reType; }; template<typename Then, typename Else> class IF_<false,Then, Else> { public: typedef Else reType; }; // 隱含要求: Condition 返回值 ret,Statement 有類型 Next template<template<typename> class Condition, typename Statement> class WHILE_ { template<typename Statement> class STOP { public: typedef Statement reType; }; public: typedef typename IF_<Condition<Statement>::ret, WHILE_<Condition, typename Statement::Next>, STOP<Statement>>::reType::reType reType; };
IF_<> 的使用示例見下面:
const int len = 4; typedef IF_<sizeof(short)==len, short, IF_<sizeof(int)==len, int, IF_<sizeof(long)==len, long, IF_<sizeof(long long)==len, long long, void>::reType>::reType>::reType>::reType int_my; // 定義一個指定字節數的類型 std::cout << sizeof(int_my) << '\n';
4
WHILE_<> 的使用示例見下面:
// 計算 1^e+2^e+...+n^e template<int n, int e> class sum_pow { template<int i, int e> class pow_e{ public: enum{ ret=i*pow_e<i,e-1>::ret }; }; template<int i> class pow_e<i,0>{ public: enum{ ret=1 }; }; // 計算 i^e,嵌套類使得能夠定義嵌套模板元函數,private 訪問控制隱藏實現細節 template<int i> class pow{ public: enum{ ret=pow_e<i,e>::ret }; }; template<typename stat> class cond { public: enum{ ret=(stat::ri<=n) }; }; template<int i, int sum> class stat { public: typedef stat<i+1, sum+pow<i>::ret> Next; enum{ ri=i, ret=sum }; }; public: enum{ ret = WHILE_<cond, stat<1,0>>::reType::ret }; }; int main() { std::cout << sum_pow<10, 2>::ret << '\n'; std::cin.get(); return 0; }
385
為了展現編譯期數值計算的強大能力,下面是一個更復雜的計算:最大公約數(Greatest Common Divisor,GCD)和最小公倍數(Lowest Common Multiple,LCM),經典的輾轉相除算法:
// 最小公倍數,普通函數 int lcm(int a, int b){ int r, lcm=a*b; while(r=a%b) { a = b; b = r; } // 因為用可變的存儲,不能寫成 a=b; b=a%b; return lcm/b; } // 遞歸函數版本 int gcd_r(int a, int b) { return b==0 ? a : gcd_r(b, a%b); } // 簡潔 int lcm_r(int a, int b) { return a * b / gcd_r(a,b); } // 模板版本 template<int a, int b> class lcm_T{ template<typename stat> class cond { public: enum{ ret=(stat::div!=0) }; }; template<int a, int b> class stat { public: typedef stat<b, a%b> Next; enum{ div=a%b, ret=b }; }; static const int gcd = WHILE_<cond, stat<a,b>>::reType::ret; public: static const int ret = a * b / gcd; }; // 遞歸模板版本 template<int a, int b> class lcm_T_r{ template<int a, int b> class gcd { public: enum{ ret = gcd<b,a%b>::ret }; }; template<int a> class gcd<a, 0> { public: enum{ ret = a }; }; public: static const int ret = a * b / gcd<a,b>::ret; }; int main() { std::cout << lcm(100, 36) << '\n'; std::cout << lcm_r(100, 36) << '\n'; std::cout << lcm_T<100, 36>::ret << '\n'; std::cout << lcm_T_r<100, 36>::ret << '\n'; std::cin.get(); return 0; }
900 900 900 900
上面例子中,定義一個類的整型常量,可以用 enum,也可以用 static const int,需要注意的是 enum 定義的常量的字節數不會超過 sizeof(int) (文獻[2])。
5. 循環展開
文獻[11]展示了一個循環展開(loop unrolling)的例子 -- 冒泡排序:
#include <utility> // std::swap // dynamic code, 普通函數版本 void bubbleSort(int* data, int n) { for(int i=n-1; i>0; --i) { for(int j=0; j<i; ++j) if (data[j]>data[j+1]) std::swap(data[j], data[j+1]); } } // 數據長度為 4 時,手動循環展開 inline void bubbleSort4(int* data) { #define COMP_SWAP(i, j) if(data[i]>data[j]) std::swap(data[i], data[j]) COMP_SWAP(0, 1); COMP_SWAP(1, 2); COMP_SWAP(2, 3); COMP_SWAP(0, 1); COMP_SWAP(1, 2); COMP_SWAP(0, 1); } // 遞歸函數版本,指導模板思路,最後一個參數是啞參數(dummy parameter),僅為分辨重載函數 class recursion { }; void bubbleSort(int* data, int n, recursion) { if(n<=1) return; for(int j=0; j<n-1; ++j) if(data[j]>data[j+1]) std::swap(data[j], data[j+1]); bubbleSort(data, n-1, recursion()); } // static code, 模板元編程版本 template<int i, int j> inline void IntSwap(int* data) { // 比較和交換兩個相鄰元素 if(data[i]>data[j]) std::swap(data[i], data[j]); } template<int i, int j> inline void IntBubbleSortLoop(int* data) { // 一次冒泡,將前 i 個元素中最大的置換到最後 IntSwap<j, j+1>(data); IntBubbleSortLoop<j<i-1?i:0, j<i-1?(j+1):0>(data); } template<> inline void IntBubbleSortLoop<0, 0>(int*) { } template<int n> inline void IntBubbleSort(int* data) { // 模板冒泡排序循環展開 IntBubbleSortLoop<n-1, 0>(data); IntBubbleSort<n-1>(data); } template<> inline void IntBubbleSort<1>(int* data) { }
對循環次數固定且比較小的循環語句,對其進行展開並內聯可以避免函數調用以及執行循環語句中的分支,從而可以提高性能,對上述代碼做如下測試,代碼在 VS2013 的 Release 下編譯運行:
#include <iostream> #include <omp.h> #include <string.h> // memcpy int main() { double t1, t2, t3; const int num=100000000; int data[4]; int inidata[4]={3,4,2,1}; t1 = omp_get_wtime(); for(int i=0; i<num; ++i) { memcpy(data, inidata, 4); bubbleSort(data, 4); } t1 = omp_get_wtime()-t1; t2 = omp_get_wtime(); for(int i=0; i<num; ++i) { memcpy(data, inidata, 4); bubbleSort4(data); } t2 = omp_get_wtime()-t2; t3 = omp_get_wtime(); for(int i=0; i<num; ++i) { memcpy(data, inidata, 4); IntBubbleSort<4>(data); } t3 = omp_get_wtime()-t3; std::cout << t1/t3 << '\t' << t2/t3 << '\n'; std::cin.get(); return 0; }
2.38643 0.926521
上述結果表明,模板元編程實現的循環展開能夠達到和手動循環展開相近的性能(90% 以上),並且性能是循環版本的 2 倍多(如果扣除 memcpy 函數占據的部分加速比將更高,根據 Amdahl 定律)。這裡可能有人會想,既然循環次數固定,為什麼不直接手動循環展開呢,難道就為了使用模板嗎?當然不是,有時候循環次數確實是編譯期固定值,但對用戶並不是固定的,比如要實現數學上向量計算的類,因為可能是 2、3、4 維,所以寫成模板,把維度作為 int 型模板參數,這時因為不知道具體是幾維的也就不得不用循環,不過因為維度信息在模板實例化時是編譯期常量且較小,所以編譯器很可能在代碼優化時進行循環展開,但我們想讓這一切發生的更可控一些。
上面用三個函數模板 IntSwap<>()、 IntBubbleSortLoop<>()、 IntBubbleSort<>() 來實現一個排序功能,不但顯得分散(和封裝原理不符),還暴露了實現細節,我們可以仿照上一節的代碼,將 IntBubbleSortLoop<>()、 IntBubbleSort<>() 嵌入其他模板內部,因為函數不允許嵌套,我們只能用類模板:
// 整合成一個類模板實現,看著好,但引入了 代碼膨脹 template<int n> class IntBubbleSortC { template<int i, int j> static inline void IntSwap(int* data) { // 比較和交換兩個相鄰元素 if(data[i]>data[j]) std::swap(data[i], data[j]); } template<int i, int j> static inline void IntBubbleSortLoop(int* data) { // 一次冒泡 IntSwap<j, j+1>(data); IntBubbleSortLoop<j<i-1?i:0, j<i-1?(j+1):0>(data); } template<> static inline void IntBubbleSortLoop<0, 0>(int*) { } public: static inline void sort(int* data) { IntBubbleSortLoop<n-1, 0>(data); IntBubbleSortC<n-1>::sort(data); } }; template<> class IntBubbleSortC<0> { public: static inline void sort(int* data) { } }; int main() { int data[4] = {3,4,2,1}; IntBubbleSortC<4>::sort(data); // 如此調用 std::cin.get(); return 0; }
上面代碼看似很好,不僅整合了代碼,借助類成員的訪問控制,還隱藏了實現細節。不過它存在著很大問題,如果實例化 IntBubbleSortC<4>、 IntBubbleSortC<3>、 IntBubbleSortC<2>,將實例化成員函數 IntBubbleSortC<4>::IntSwap<0, 1>()、 IntBubbleSortC<4>::IntSwap<1, 2>()、 IntBubbleSortC<4>::IntSwap<2, 3>()、 IntBubbleSortC<3>::IntSwap<0, 1>()、 IntBubbleSortC<3>::IntSwap<1, 2>()、 IntBubbleSortC<2>::IntSwap<0, 1>(),而在原來的看著分散的代碼中 IntSwap<0, 1>() 只有一個。這將導致代碼膨脹(code bloat),即生成的可執行文件體積變大(代碼膨脹另一含義是源代碼增大,見文獻[1]第11章)。不過這裡使用了內聯(inline),如果編譯器確實內聯展開代碼則不會導致代碼膨脹(除了循環展開本身會帶來的代碼膨脹),但因為重復編譯原本可以復用的模板實例,會增加編譯時間。在上一節的例子中,因為只涉及編譯期常量計算,並不涉及函數(函數模板,或類模板的成員函數,函數被編譯成具體的機器二進制代碼),並不會出現代碼膨脹。
為了清晰證明上面的論述,我們去掉所有 inline 並將函數實現放到類外面(類裡面實現的成員函數都是內聯的,因為函數實現可能被包含多次,見文獻[2] 10.2.9,不過現在的編譯器優化能力很強,很多時候加不加 inline 並不影響編譯器自己對內聯的選擇...),分別編譯分散版本和類模板封裝版本的冒泡排序代碼編譯生成的目標文件(VS2013 下是 .obj 文件)的大小,代碼均在 VS2013 Debug 模式下編譯(防止編譯器優化),比較 main.obj (源文件是 main.cpp)大小。
類模板封裝版本代碼如下,注意將成員函數在外面定義的寫法:
#include <iostream> #include <utility> // std::swap // 整合成一個類模板實現,看著好,但引入了 代碼膨脹 template<int n> class IntBubbleSortC { template<int i, int j> static void IntSwap(int* data); template<int i, int j> static void IntBubbleSortLoop(int* data); template<> static void IntBubbleSortLoop<0, 0>(int*) { } public: static void sort(int* data); }; template<> class IntBubbleSortC<0> { public: static void sort(int* data) { } }; template<int n> template<int i, int j> void IntBubbleSortC<n>::IntSwap(int* data) { if(data[i]>data[j]) std::swap(data[i], data[j]); } template<int n> template<int i, int j> void IntBubbleSortC<n>::IntBubbleSortLoop(int* data) { IntSwap<j, j+1>(data); IntBubbleSortLoop<j<i-1?i:0, j<i-1?(j+1):0>(data); } template<int n> void IntBubbleSortC<n>::sort(int* data) { IntBubbleSortLoop<n-1, 0>(data); IntBubbleSortC<n-1>::sort(data); } int main() { int data[40] = {3,4,2,1}; IntBubbleSortC<2>::sort(data); IntBubbleSortC<3>::sort(data); IntBubbleSortC<4>::sort(data); IntBubbleSortC<5>::sort(data); IntBubbleSortC<6>::sort(data); IntBubbleSortC<7>::sort(data); IntBubbleSortC<8>::sort(data); IntBubbleSortC<9>::sort(data); IntBubbleSortC<10>::sort(data); IntBubbleSortC<11>::sort(data); #if 0 IntBubbleSortC<12>::sort(data); IntBubbleSortC<13>::sort(data); IntBubbleSortC<14>::sort(data); IntBubbleSortC<15>::sort(data); IntBubbleSortC<16>::sort(data); IntBubbleSortC<17>::sort(data); IntBubbleSortC<18>::sort(data); IntBubbleSortC<19>::sort(data); IntBubbleSortC<20>::sort(data); IntBubbleSortC<21>::sort(data); IntBubbleSortC<22>::sort(data); IntBubbleSortC<23>::sort(data); IntBubbleSortC<24>::sort(data); IntBubbleSortC<25>::sort(data); IntBubbleSortC<26>::sort(data); IntBubbleSortC<27>::sort(data); IntBubbleSortC<28>::sort(data); IntBubbleSortC<29>::sort(data); IntBubbleSortC<30>::sort(data); IntBubbleSortC<31>::sort(data); #endif std::cin.get(); return 0; }
分散定義函數模板版本代碼如下,為了更具可比性,也將函數放在類裡面作為成員函數:
#include <iostream> #include <utility> // std::swap // static code, 模板元編程版本 template<int i, int j> class IntSwap { public: static void swap(int* data); }; template<int i, int j> class IntBubbleSortLoop { public: static void loop(int* data); }; template<> class IntBubbleSortLoop<0, 0> { public: static void loop(int* data) { } }; template<int n> class IntBubbleSort { public: static void sort(int* data); }; template<> class IntBubbleSort<0> { public: static void sort(int* data) { } }; template<int i, int j> void IntSwap<i, j>::swap(int* data) { if(data[i]>data[j]) std::swap(data[i], data[j]); } template<int i, int j> void IntBubbleSortLoop<i, j>::loop(int* data) { IntSwap<j, j+1>::swap(data); IntBubbleSortLoop<j<i-1?i:0, j<i-1?(j+1):0>::loop(data); } template<int n> void IntBubbleSort<n>::sort(int* data) { IntBubbleSortLoop<n-1, 0>::loop(data); IntBubbleSort<n-1>::sort(data); } int main() { int data[40] = {3,4,2,1}; IntBubbleSort<2>::sort(data); IntBubbleSort<3>::sort(data); IntBubbleSort<4>::sort(data); IntBubbleSort<5>::sort(data); IntBubbleSort<6>::sort(data); IntBubbleSort<7>::sort(data); IntBubbleSort<8>::sort(data); IntBubbleSort<9>::sort(data); IntBubbleSort<10>::sort(data); IntBubbleSort<11>::sort(data); #if 0 IntBubbleSort<12>::sort(data); IntBubbleSort<13>::sort(data); IntBubbleSort<14>::sort(data); IntBubbleSort<15>::sort(data); IntBubbleSort<16>::sort(data); IntBubbleSort<17>::sort(data); IntBubbleSort<18>::sort(data); IntBubbleSort<19>::sort(data); IntBubbleSort<20>::sort(data); IntBubbleSort<21>::sort(data); IntBubbleSort<22>::sort(data); IntBubbleSort<23>::sort(data); IntBubbleSort<24>::sort(data); IntBubbleSort<25>::sort(data); IntBubbleSort<26>::sort(data); IntBubbleSort<27>::sort(data); IntBubbleSort<28>::sort(data); IntBubbleSort<29>::sort(data); IntBubbleSort<30>::sort(data); IntBubbleSort<31>::sort(data); #endif std::cin.get(); return 0; }
程序中條件編譯都未打開時(#if 0),main.obj 大小分別為 264 KB 和 211 KB,條件編譯打開時(#if 1),main.obj 大小分別為 1073 KB 和 620 KB。可以看到,類模板封裝版的對象文件不但絕對大小更大,而且增長更快,這和之前分析是一致的。
6. 表達式模板,向量運算
文獻[12]展示了一個表達式模板(Expression Templates)的例子:
#include <iostream> // std::cout #include <cmath> // std::sqrt() // 表達式類型 class DExprLiteral { // 文字量 double a_; public: DExprLiteral(double a) : a_(a) { } double operator()(double x) const { return a_; } }; class DExprIdentity { // 自變量 public: double operator()(double x) const { return x; } }; template<class A, class B, class Op> // 雙目操作 class DBinExprOp { A a_; B b_; public: DBinExprOp(const A& a, const B& b) : a_(a), b_(b) { } double operator()(double x) const { return Op::apply(a_(x), b_(x)); } }; template<class A, class Op> // 單目操作 class DUnaryExprOp { A a_; public: DUnaryExprOp(const A& a) : a_(a) { } double operator()(double x) const { return Op::apply(a_(x)); } }; // 表達式 template<class A> class DExpr { A a_; public: DExpr() { } DExpr(const A& a) : a_(a) { } double operator()(double x) const { return a_(x); } }; // 運算符,模板參數 A、B 為參與運算的表達式類型 // operator /, division class DApDiv { public: static double apply(double a, double b) { return a / b; } }; template<class A, class B> DExpr<DBinExprOp<DExpr<A>, DExpr<B>, DApDiv> > operator/(const DExpr<A>& a, const DExpr<B>& b) { typedef DBinExprOp<DExpr<A>, DExpr<B>, DApDiv> ExprT; return DExpr<ExprT>(ExprT(a, b)); } // operator +, addition class DApAdd { public: static double apply(double a, double b) { return a + b; } }; template<class A, class B> DExpr<DBinExprOp<DExpr<A>, DExpr<B>, DApAdd> > operator+(const DExpr<A>& a, const DExpr<B>& b) { typedef DBinExprOp<DExpr<A>, DExpr<B>, DApAdd> ExprT; return DExpr<ExprT>(ExprT(a, b)); } // sqrt(), square rooting class DApSqrt { public: static double apply(double a) { return std::sqrt(a); } }; template<class A> DExpr<DUnaryExprOp<DExpr<A>, DApSqrt> > sqrt(const DExpr<A>& a) { typedef DUnaryExprOp<DExpr<A>, DApSqrt> ExprT; return DExpr<ExprT>(ExprT(a)); } // operator-, negative sign class DApNeg { public: static double apply(double a) { return -a; } }; template<class A> DExpr<DUnaryExprOp<DExpr<A>, DApNeg> > operator-(const DExpr<A>& a) { typedef DUnaryExprOp<DExpr<A>, DApNeg> ExprT; return DExpr<ExprT>(ExprT(a)); } // evaluate() template<class Expr> void evaluate(const DExpr<Expr>& expr, double start, double end, double step) { for(double i=start; i<end; i+=step) std::cout << expr(i) << ' '; } int main() { DExpr<DExprIdentity> x; evaluate( -x / sqrt( DExpr<DExprLiteral>(1.0) + x ) , 0.0, 10.0, 1.0); std::cin.get(); return 0; }
-0 -0.707107 -1.1547 -1.5 -1.78885 -2.04124 -2.26779 -2.47487 -2.66667 -2.84605
代碼有點長(我已經盡量壓縮行數),請先看最下面的 main() 函數,表達式模板允許我們以 “-x / sqrt( 1.0 + x )” 這種類似數學表達式的方式傳參數,在 evaluate() 內部,將 0-10 的數依次賦給自變量 x 對表達式進行求值,這是通過在 template<> DExpr 類模板內部重載 operator() 實現的。我們來看看這一切是如何發生的。
在 main() 中調用 evaluate() 時,編譯器根據全局重載的加號、sqrt、除號、負號推斷“-x / sqrt( 1.0 + x )” 的類型是 Dexpr<DBinExprOp<Dexpr<DUnaryExprOp<Dexpr<DExprIdentity>, DApNeg>>, Dexpr<DUnaryExprOp<Dexpr<DBinExprOp<Dexpr<DExprLiteral>, Dexpr<DExprIdentity>, DApAdd>>, DApSqrt>>, DApDiv>>(即將每個表達式編碼到一種類型,設這個類型為 ultimateExprType),並用此類型實例化函數模板 evaluate(),類型的推導見下圖。在 evaluate() 中,對表達式進行求值 expr(i),調用 ultimateExprType 的 operator(),這引起一系列的 operator() 和 Op::apply() 的調用,最終遇到基礎類型 “表達式類型” DExprLiteral 和 DExprIdentity,這個過程見下圖。總結就是,請看下圖,從下到上類型推斷,從上到下 operator() 表達式求值。
#include <iostream> // std::cout #include <cmath> // std::sqrt() void evaluate(double start, double end, double step) { double _temp = 1.0; for(double i=start; i<end; i+=step) std::cout << -i / std::sqrt(_temp + i) << ' '; } int main() { evaluate(0.0, 10.0, 1.0); std::cin.get(); return 0; }
-0 -0.707107 -1.1547 -1.5 -1.78885 -2.04124 -2.26779 -2.47487 -2.66667 -2.84605
和表達式模板類似的技術還可以用到向量計算中,以避免產生臨時向量變量,見文獻[4] Expression templates 和文獻[12]的後面。傳統向量計算如下:
class DoubleVec; // DoubleVec 重載了 + - * / 等向量元素之間的計算 DoubleVec y(1000), a(1000), b(1000), c(1000), d(1000); // 向量長度 1000 // 向量計算 y = (a + b) / (c - d); // 等價於 DoubleVec __t1 = a + b; DoubleVec __t2 = c - d; DoubleVec __t3 = __t1 / __t2; y = __t3;
模板代碼實現向量計算如下:
template<class A> DVExpr; class DVec{ // ... template<class A> DVec& operator=(const DVExpr<A>&); // 由 = 引起向量逐個元素的表達式值計算並賦值 }; DVec y(1000), a(1000), b(1000), c(1000), d(1000); // 向量長度 1000 // 向量計算 y = (a + b) / (c - d); // 等價於 for(int i=0; i<1000; ++i) { y[i] = (a[i] + b[i]) / (c[i] + d[i]); }
不過值得一提的是,傳統代碼可以用 C++11 的右值引用提升性能,C++11 新特性我們以後再詳細討論。
我們這裡看下文獻[4] Expression templates 實現的版本,它用到了編譯期多態,編譯期多態示意代碼如下(關於這種代碼形式有個名字叫 curiously recurring template pattern, CRTP,見文獻[4]):
// 模板基類,定義接口,具體實現由模板參數,即子類實現 template <typename D> class base { public: void f1() { static_cast<E&>(*this).f1(); } // 直接調用子類實現 int f2() const { static_cast<const E&>(*this).f1(); } }; // 子類 class dirived1 : public base<dirived1> { public: void f1() { /* ... */ } int f2() const { /* ... */ } }; template<typename T> class dirived2 : public base<dirived2<T>> { public: void f1() { /* ... */ } int f2() const { /* ... */ } };
簡化後(向量長度固定為1000,元素類型為 double)的向量計算代碼如下:
#include <iostream> // std::cout // A CRTP base class for Vecs with a size and indexing: template <typename E> class VecExpr { public: double operator[](int i) const { return static_cast<E const&>(*this)[i]; } operator E const&() const { return static_cast<const E&>(*this); } // 向下類型轉換 }; // The actual Vec class: class Vec : public VecExpr<Vec> { double _data[1000]; public: double& operator[](int i) { return _data[i]; } double operator[](int i) const { return _data[i]; } template <typename E> Vec const& operator=(VecExpr<E> const& vec) { E const& v = vec; for (int i = 0; i<1000; ++i) _data[i] = v[i]; return *this; } // Constructors Vec() { } Vec(double v) { for(int i=0; i<1000; ++i) _data[i] = v; } }; template <typename E1, typename E2> class VecDifference : public VecExpr<VecDifference<E1, E2> > { E1 const& _u; E2 const& _v; public: VecDifference(VecExpr<E1> const& u, VecExpr<E2> const& v) : _u(u), _v(v) { } double operator[](int i) const { return _u[i] - _v[i]; } }; template <typename E> class VecScaled : public VecExpr<VecScaled<E> > { double _alpha; E const& _v; public: VecScaled(double alpha, VecExpr<E> const& v) : _alpha(alpha), _v(v) { } double operator[](int i) const { return _alpha * _v[i]; } }; // Now we can overload operators: template <typename E1, typename E2> VecDifference<E1, E2> const operator-(VecExpr<E1> const& u, VecExpr<E2> const& v) { return VecDifference<E1, E2>(u, v); } template <typename E> VecScaled<E> const operator*(double alpha, VecExpr<E> const& v) { return VecScaled<E>(alpha, v); } int main() { Vec u(3), v(1); double alpha=9; Vec y; y = alpha*(u - v); std::cout << y[999] << '\n'; std::cin.get(); return 0; }
18
“alpha*(u - v)” 的類型推斷過程如下圖所示,其中有子類到基類的隱式類型轉換:
7. 特性,策略,標簽
利用迭代器,我們可以實現很多通用算法,迭代器在容器與算法之間搭建了一座橋梁。求和函數模板如下:
#include <iostream> // std::cout #include <vector> template<typename iter> typename iter::value_type mysum(iter begin, iter end) { typename iter::value_type sum(0); for(iter i=begin; i!=end; ++i) sum += *i; return sum; } int main() { std::vector<int> v; for(int i = 0; i<100; ++i) v.push_back(i); std::cout << mysum(v.begin(), v.end()) << '\n'; std::cin.get(); return 0; }
4950
我們想讓 mysum() 對指針參數也能工作,畢竟迭代器就是模擬指針,但指針沒有嵌套類型 value_type,可以定義 mysum() 對指針類型的特例,但更好的辦法是在函數參數和 value_type 之間多加一層 -- 特性(traits)(參考了文獻[1]第72頁,特性詳見文獻[1] 12.1):
// 特性,traits template<typename iter> class mytraits{ public: typedef typename iter::value_type value_type; }; template<typename T> class mytraits<T*>{ public: typedef T value_type; }; template<typename iter> typename mytraits<iter>::value_type mysum(iter begin, iter end) { typename mytraits<iter>::value_type sum(0); for(iter i=begin; i!=end; ++i) sum += *i; return sum; } int main() { int v[4] = {1,2,3,4}; std::cout << mysum(v, v+4) << '\n'; std::cin.get(); return 0; }
10
其實,C++ 標准定義了類似的 traits:std::iterator_trait(另一個經典例子是 std::numeric_limits) 。特性對類型的信息(如 value_type、 reference)進行包裝,使得上層代碼可以以統一的接口訪問這些信息。C++ 模板元編程會涉及大量的類型計算,很多時候要提取類型的信息(typedef、 常量值等),如果這些類型的信息的訪問方式不一致(如上面的迭代器和指針),我們將不得不定義特例,這會導致大量重復代碼的出現(另一種代碼膨脹),而通過加一層特性可以很好的解決這一問題。另外,特性不僅可以對類型的信息進行包裝,還可以提供更多信息,當然,因為加了一層,也帶來復雜性。特性是一種提供元信息的手段。
策略(policy)一般是一個類模板,典型的策略是 STL 容器(如 std::vector<>,完整聲明是template<class T, class Alloc=allocator<T>> class vector;)的分配器(這個參數有默認參數,即默認存儲策略),策略類將模板的經常變化的那一部分子功能塊集中起來作為模板參數,這樣模板便可以更為通用,這和特性的思想是類似的(詳見文獻[1] 12.3)。
標簽(tag)一般是一個空類,其作用是作為一個獨一無二的類型名字用於標記一些東西,典型的例子是 STL 迭代器的五種類型的名字(input_iterator_tag, output_iterator_tag, forward_iterator_tag, bidirectional_iterator_tag, random_access_iterator_tag),std::vector<int>::iterator::iterator_category 就是 random_access_iterator_tag,可以用第1節判斷類型是否等價的模板檢測這一點:
#include <iostream> #include <vector> template<typename T1, typename T2> // 通例,返回 false class theSameType { public: enum { ret = false }; }; template<typename T> // 特例,兩類型相同時返回 true class theSameType<T, T> { public: enum { ret = true }; }; int main(){ std::cout << theSameType< std::vector<int>::iterator::iterator_category, std::random_access_iterator_tag >::ret << '\n'; std::cin.get(); return 0; }
1
有了這樣的判斷,還可以根據判斷結果做更復雜的元編程邏輯(如一個算法以迭代器為參數,根據迭代器標簽進行特例化以對某種迭代器特殊處理)。標簽還可以用來分辨函數重載,第5節中就用到了這樣的標簽(recursion)(標簽詳見文獻[1] 12.1)。
8. 更多類型計算
在第1節我們講類型等價的時候,已經見到了一個可以判斷兩個類型是否等價的模板,這一節我們給出更多例子,下面是判斷一個類型是否可以隱式轉換到另一個類型的模板(參考了文獻[6] Static interface checking):
#include <iostream> // std::cout // whether T could be converted to U template<class T, class U> class ConversionTo { typedef char Type1[1]; // 兩種 sizeof 不同的類型 typedef char Type2[2]; static Type1& Test( U ); // 較下面的函數,因為參數取值范圍小,優先匹配 static Type2& Test(...); // 變長參數函數,可以匹配任何數量任何類型參數 static T MakeT(); // 返回類型 T,用這個函數而不用 T() 因為 T 可能沒有默認構造函數 public: enum { ret = sizeof(Test(MakeT()))==sizeof(Type1) }; // 可以轉換時調用返回 Type1 的 Test() }; int main() { std::cout << ConversionTo<int, double>::ret << '\n'; std::cout << ConversionTo<float, int*>::ret << '\n'; std::cout << ConversionTo<const int&, int&>::ret << '\n'; std::cin.get(); return 0; }
1 0 0
下面這個例子檢查某個類型是否含有某個嵌套類型定義(參考了文獻[4] Substitution failure is not an erro (SFINAE)),這個例子是個內省(反射的一種):
#include <iostream> #include <vector> // thanks to Substitution failure is not an erro (SFINAE) template<typename T> struct has_typedef_value_type { typedef char Type1[1]; typedef char Type2[2]; template<typename C> static Type1& test(typename C::value_type*); template<typename> static Type2& test(...); public: static const bool ret = sizeof(test<T>(0)) == sizeof(Type1); // 0 == NULL }; struct foo { typedef float lalala; }; int main() { std::cout << has_typedef_value_type<std::vector<int>>::ret << '\n'; std::cout << has_typedef_value_type<foo>::ret << '\n'; std::cin.get(); return 0; }
1 0
這個例子是有缺陷的,因為不存在引用的指針,所以不用用來檢測引用類型定義。可以看到,因為只涉及類型推斷,都是編譯期的計算,不涉及任何可執行代碼,所以類的成員函數根本不需要具體實現。
9. 元容器
文獻[1]第 13 章講了元容器,所謂元容器,就是類似於 std::vector<> 那樣的容器,不過它存儲的是元數據 -- 類型,有了元容器,我們就可以判斷某個類型是否屬於某個元容器之類的操作。
在講元容器之前,我們先來看看偽變長參數模板(文獻[1] 12.4),一個可以存儲小於某個數(例子中為 4 個)的任意個數,任意類型數據的元組(tuple)的例子如下(參考了文獻[1] 第 225~227 頁):
#include <iostream> class null_type {}; // 標簽類,標記參數列表末尾 template<typename T0, typename T1, typename T2, typename T3> class type_shift_node { public: typedef T0 data_type; typedef type_shift_node<T1, T2, T3, null_type> next_type; // 參數移位了 static const int num = next_type::num + 1; // 非 null_type 模板參數個數 data_type data; // 本節點數據 next_type next; // 後續所有節點數據 type_shift_node() :data(), next() { } // 構造函數 type_shift_node(T0 const& d0, T1 const& d1, T2 const& d2, T3 const& d3) :data(d0), next(d1, d2, d3, null_type()) { } // next 參數也移位了 }; template<typename T0> // 特例,遞歸終止 class type_shift_node<T0, null_type, null_type, null_type> { public: typedef T0 data_type; static const int num = 1; data_type data; // 本節點數據 type_shift_node() :data(), next() { } // 構造函數 type_shift_node(T0 const& d0, null_type, null_type, null_type) : data(d0) { } }; // 元組類模板,默認參數 + 嵌套遞歸 template<typename T0, typename T1=null_type, typename T2=null_type, typename T3=null_type> class my_tuple { public: typedef type_shift_node<T0, T1, T2, T3> tuple_type; static const int num = tuple_type::num; tuple_type t; my_tuple(T0 const& d0=T0(),T1 const& d1=T1(),T2 const& d2=T2(),T3 const& d3=T3()) : t(d0, d1, d2, d3) { } // 構造函數,默認參數 }; // 為方便訪問元組數據,定義 get<unsigned>(tuple) 函數模板 template<unsigned i, typename T0, typename T1, typename T2, typename T3> class type_shift_node_traits { public: typedef typename type_shift_node_traits<i-1,T0,T1,T2,T3>::node_type::next_type node_type; typedef typename node_type::data_type data_type; static node_type& get_node(type_shift_node<T0,T1,T2,T3>& node) { return type_shift_node_traits<i-1,T0,T1,T2,T3>::get_node(node).next; } }; template<typename T0, typename T1, typename T2, typename T3> class type_shift_node_traits<0, T0, T1, T2, T3> { public: typedef typename type_shift_node<T0,T1,T2,T3> node_type; typedef typename node_type::data_type data_type; static node_type& get_node(type_shift_node<T0,T1,T2,T3>& node) { return node; } }; template<unsigned i, typename T0, typename T1, typename T2, typename T3> typename type_shift_node_traits<i,T0,T1,T2,T3>::data_type get(my_tuple<T0,T1,T2,T3>& tup) { return type_shift_node_traits<i,T0,T1,T2,T3>::get_node(tup.t).data; } int main(){ typedef my_tuple<int, char, float> tuple3; tuple3 t3(10, 'm', 1.2f); std::cout << t3.t.data << ' ' << t3.t.next.data << ' ' << t3.t.next.next.data << '\n'; std::cout << tuple3::num << '\n'; std::cout << get<2>(t3) << '\n'; // 從 0 開始,不要出現 3,否則將出現不可理解的編譯錯誤 std::cin.get(); return 0; }
10 m 1.2 3 1.2
C++11 引入了變長模板參數,其背後的原理也是模板遞歸(文獻[1]第 230 頁)。
利用和上面例子類似的模板參數移位遞歸的原理,我們可以構造一個存儲“類型”的元組,即元容器,其代碼如下(和文獻[1]第 237 頁的例子不同):
#include <iostream> // 元容器 template<typename T0=void, typename T1=void, typename T2=void, typename T3=void> class meta_container { public: typedef T0 type; typedef meta_container<T1, T2, T3, void> next_node; // 參數移位了 static const int size = next_node::size + 1; // 非 null_type 模板參數個數 }; template<> // 特例,遞歸終止 class meta_container<void, void, void, void> { public: typedef void type; static const int size = 0; }; // 訪問元容器中的數據 template<typename C, unsigned i> class get { public: static_assert(i<C::size, "get<C,i>: index exceed num"); // C++11 引入靜態斷言 typedef typename get<C,i-1>::c_type::next_node c_type; typedef typename c_type::type ret_type; }; template<typename C> class get<C, 0> { public: static_assert(0<C::size, "get<C,i>: index exceed num"); // C++11 引入靜態斷言 typedef C c_type; typedef typename c_type::type ret_type; }; // 在元容器中查找某個類型,找到返回索引,找不到返回 -1 template<typename T1, typename T2> class same_type { public: enum { ret = false }; }; template<typename T> class same_type<T, T> { public: enum { ret = true }; }; template<bool c, typename Then, typename Else> class IF_ { }; template<typename Then, typename Else> class IF_<true, Then, Else> { public: typedef Then reType; }; template<typename Then, typename Else> class IF_<false, Then, Else> { public: typedef Else reType; }; template<typename C, typename T> class find { template<int i> class number { public: static const int ret = i; }; template<typename C, typename T, int i> class find_i { public: static const int ret = IF_< same_type<get<C,i>::ret_type, T>::ret, number<i>, find_i<C,T,i-1> >::reType::ret; }; template<typename C, typename T> class find_i<C, T, -1> { public: static const int ret = -1; }; public: static const int ret = find_i<C, T, C::size-1>::ret; }; int main(){ typedef meta_container<int, int&, const int> mc; int a = 9999; get<mc, 1>::ret_type aref = a; std::cout << mc::size << '\n'; std::cout << aref << '\n'; std::cout << find<mc, const int>::ret << '\n'; std::cout << find<mc, float>::ret << '\n'; std::cin.get(); return 0; }
3 9999 2 -1
上面例子已經實現了存儲類型的元容器,和元容器上的查找算法,但還有一個小問題,就是它不能處理模板,編譯器對模板的操縱能力遠不如對類型的操縱能力強(提示:類模板實例是類型),我們可以一種間接方式實現存儲“模板元素”,即用模板的一個代表實例(如全用 int 為參數的實例)來代表這個模板,這樣對任意模板實例,只需判斷其模板的代表實例是否在容器中即可,這需要進行類型過濾:對任意模板的實例將其替換為指定模板參數的代表實例,類型過濾實例代碼如下(參考了文獻[1]第 241 頁):
// 類型過濾,meta_filter 使用時只用一個參數,設置四個模板參數是因為,模板通例的參數列表 // 必須能夠包含特例參數列表,後面三個參數設置默認值為 void 或標簽模板 template<typename T> class dummy_template_1 {}; template<typename T0, typename T1> class dummy_template_2 {}; template<typename T0, typename T1 = void, template<typename> class tmp_1 = dummy_template_1, template<typename, typename> class tmp_2 = dummy_template_2> class meta_filter { // 通例,不改變類型 public: typedef T0 ret_type; }; // 匹配任何帶有一個類型參數模板的實例,將模板實例替換為代表實例 template<template<typename> class tmp_1, typename T> class meta_filter<tmp_1<T>, void, dummy_template_1, dummy_template_2> { public: typedef tmp_1<int> ret_type; }; // 匹配任何帶有兩個類型參數模板的實例,將模板實例替換為代表實例 template<template<typename, typename> class tmp_2, typename T0, typename T1> class meta_filter<tmp_2<T0, T1>, void, dummy_template_1, dummy_template_2> { public: typedef tmp_2<int, int> ret_type; };
現在,只需將上面元容器和元容器查找函數修改為:對模板實例將其換為代表實例,即修改 meta_container<> 通例中“typedef T0 type;”語句為“typedef typename meta_filter<T0>::ret_type type;”,修改 find<> 的最後一行中“T”為“typename meta_filter<T>::ret_type”。修改後,下面代碼的執行結果是:
template<typename, typename> class my_tmp_2; // 自動將 my_tmp_2<float, int> 過濾為 my_tmp_2<int, int> typedef meta_container<int, float, my_tmp_2<float, int>> mc2; // 自動將 my_tmp_2<char, double> 過濾為 my_tmp_2<int, int> std::cout << find<mc2, my_tmp_2<char, double>>::ret << '\n'; // 輸出 2
2
10. 總結
博文比較長,總結一下所涉及的東西:
進一步學習
C++ 確實比較復雜,這可能是因為,雖然 C++ 語言層次比較低,但它卻同時可以實現很多高級特性。進一步學習 C++ 模板元編程的途徑很多:
參考文獻:
注:參考文獻中所給的鏈接,打開不了的,可以參見我的另一篇博客配置浏覽器
Johannes Koskinen 論文,Stanford 課程 PPT,Todd Veldhuizen 論文我網盤保存的副本 -
鏈接: http://pan.baidu.com/s/1ntJstvF 密碼: hogb