C++模板超程式設計
所謂的超程式設計就是編寫直接生成或操縱程式的程式,C++模板為C++語言提供了超程式設計的能力。模板使 C++ 程式設計變得異常靈活,能實現很多高階動態語言才有的特性(語法上可能比較醜陋,一些歷史原因見下文)。普通使用者對 C++ 模板的使用可能不是很頻繁,大致限於泛型程式設計,但一些系統級的程式碼,尤其是對通用性、效能要求極高的基礎庫(如 STL、Boost)幾乎不可避免的都大量地使用 C++ 模板,一個稍有規模的大量使用模板的程式,不可避免的要涉及超程式設計(如型別計算)。本文就是要剖析 C++ 模板超程式設計的機制。
1. C++模板的語法
函式模板(function 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;
}
<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;
}
程式輸出如下:
<span style="color:#000000">1
0</span>
關於模板例項化(template instantiation)(詳見文獻[4]模板):
- 指在編譯或連結時生成函式模板或類模板的具體例項原始碼,即用使用模板時的實參型別替換模板型別引數(還有非型別引數和模板型引數);
- 隱式例項化(implicit instantiation):當使用例項化的模板時自動地在當前程式碼單元之前插入模板的例項化程式碼,模板的成員函式一直到引用時才被例項化;
- 顯式例項化(explicit instantiation):直接宣告模板例項化,模板所有成員立即都被例項化;
- 例項化也是一種特例化,被稱為例項化的特例(instantiated (or generated) specialization)。
隱式例項化時,成員只有被引用到才會進行例項化,這被稱為推遲例項化(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;
}
<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;
}
<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;
}
<span style="color:#000000">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</span>
上面的錯誤是因為,當編譯 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;
}
<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;
}
<span style="color:#000000">3228800</span>
關於模板的編譯和連結(詳見文獻[1] 1.3、文獻[4]模板):
- 包含模板編譯模式:編譯器生成每個編譯單元中遇到的所有的模板例項,並存放在相應的目標檔案中;連結器合併等價的模板例項,生成可執行檔案,要求例項化時模板定義可見,不能使用系統連結器;
- 分離模板編譯模式(使用 export 關鍵字):不重複生成模板例項,編譯器設計要求高,可以使用系統連結器;
- 包含編譯模式是主流,C++11 已經棄用 export 關鍵字(對模板引入 extern 新用法),一般將模板的全部實現程式碼放在同一個標頭檔案中並在用到模板的地方用 #include 包含標頭檔案,以防止出現例項不一致(如下面緊接著例子);
例項化,編譯連結的簡單例子如下(參考了文獻[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;
}
#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>
}
#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>
}
<span style="color:#000000">a.cpp: 1
a.cpp: 0.1
b.cpp: 2
a.cpp: 0.1</span>
上例中,由於 a.cpp 和 b.cpp 中的 print<double> 例項等價(模板例項的二進位制程式碼在編譯生成的物件檔案 a.obj、b.obj 中),故連結時消除了一個(消除哪個沒有規定,上面消除了 b.cpp 中的)。
關於 template、typename、this 關鍵字的使用(文獻[4]模板,文獻[5]):
- 依賴於模板引數(template parameter,形式引數,實參英文為 argument)的名字被稱為依賴名字(dependent name),C++標準規定,如果解析器在一個模板中遇到一個巢狀依賴名字,它假定那個名字不是一個型別,除非顯式用 typename 關鍵字前置修飾該名字;
- 和上一條 typename 用法類似,template 用於指明巢狀型別或函式為模板;
- this 用於指定查詢基類中的成員(當基類是依賴模板引數的類模板例項時,由於例項化總是推遲,這時不依賴模板引數的名字不在基類中查詢,文獻[1]第 166 頁)。
一個例子如下(需要 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;
}
<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;
}
<span style="color:#000000">global f()
member f(): 99
member f(): 22
global f()</span>
C++11 關於模板的新特性(詳見文獻[1]第15章,文獻[4]C++11):
- “>>” 根據上下文自動識別正確語義;
- 函式模板引數預設值;
- 變長模板引數(擴充套件 sizeof...() 獲取引數個數);
- 模板別名(擴充套件 using 關鍵字);
- 外部模板例項(拓展 extern 關鍵字),棄用 export template。
在本文中,如無特別宣告將不使用 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]),可以看到關鍵是模板的機制在編譯具體程式碼(模板例項)前執行:
從程式設計範型(programming paradigm)上來說,C++ 模板是函數語言程式設計(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;
}
<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;
}
<span style="color:#000000">8
125250
someComputing: i=99</span>
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()
}
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()
}
<span style="color:#000000">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; }
^
<strong>main.cpp:2:28: warning: initializing argument 1 of 'D<i>::D(void*) [with int i = 2]' [-fpermissive]</strong>
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(); }
^
<strong>main.cpp:2:28: warning: initializing argument 1 of 'D<i>::D(void*) [with int i = 7]' [-fpermissive]</strong>
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(); }
^
<strong>main.cpp:2:28: w