C++模板元编程(C++ template metaprogramming)
所谓元编程就是编写直接生成或操纵程序的程序,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;
} 程序输出如下:
关于模板(函数模板、类模板)的模板参数(详见文献第3章):
类型参数(type template parameter),用 typename 或 class 标记;
非类型参数(non-type template parameter)可以是:整数及枚举类型、对象或函数的指针、对象或函数的引用、对象的成员指针,非类型参数是模板实例的常量;
模板型参数(template template parameter),如“template<typename T, template<typename> class A> someclass {};”;
模板参数可以有默认值(函数模板参数默认是从 C++11 开始支持);
函数模板的和函数参数类型有关的模板参数可以自动推导,类模板参数不存在推导机制; C++11 引入变长模板参数,请见下文。
模板特例化(template specialization,又称特例、特化)的简单示例如下:
// 实现一个向量类
template<typename T, int N>
class Vec{
T _v;
// ... // 模板通例(primary template),具体实现
};
template<>
class Vec<float, 4>{
float _v;
// ... // 对 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的某个真子集进行专门定义。 更多模板特例化的例子如下(参考了文献第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>; 关于模板特例化(详见文献第4章):
在定义模板特例之前必须已经有模板通例(primary template)的声明;
模板特例并不要求一定与通例有相同的接口,但为了方便使用(体会特例的语义)一般都相同;
匹配规则,在模板实例化时如果有模板通例、特例加起来多个模板版本可以匹配,则依据如下规则:对版本AB,如果 A 的模板参数取值集合是B的真子集,则优先匹配 A,如果 AB 的模板参数取值集合是“交叉”关系(AB 交集不为空,且不为包含关系),则发生编译错误,对于函数模板,用函数重载分辨(overload resolution)规则和上述规则结合并优先匹配非模板函数。 对模板的多个实例,类型等价(type equivalence)判断规则(详见文献 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)(详见文献模板):
指在编译或链接时生成函数模板或类模板的具体实例源代码,即用使用模板时的实参类型替换模板类型参数(还有非类型参数和模板型参数);
隐式实例化(implicit instantiation):当使用实例化的模板时自动地在当前代码单元之前插入模板的实例化代码,模板的成员函数一直到引用时才被实例化;
显式实例化(explicit instantiation):直接声明模板实例化,模板所有成员立即都被实例化;
实例化也是一种特例化,被称为实例化的特例(instantiated (or generated) specialization)。 隐式实例化时,成员只有被引用到才会进行实例化,这被称为推迟实例化(lazy instantiation),由此可能带来的问题如下面的例子(文献,文献):
#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 多行错误信息):
上面的错误是因为,当编译 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.3、文献模板):
包含模板编译模式:编译器生成每个编译单元中遇到的所有的模板实例,并存放在相应的目标文件中;链接器合并等价的模板实例,生成可执行文件,要求实例化时模板定义可见,不能使用系统链接器;
分离模板编译模式(使用 export 关键字):不重复生成模板实例,编译器设计要求高,可以使用系统链接器;
包含编译模式是主流,C++11 已经弃用 export 关键字(对模板引入 extern 新用法),一般将模板的全部实现代码放在同一个头文件中并在用到模板的地方用 #include 包含头文件,以防止出现实例不一致(如下面紧接着例子); 实例化,编译链接的简单例子如下(参考了文献第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>
}
1
2
3
4
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 关键字的使用(文献模板,文献):
依赖于模板参数(template parameter,形式参数,实参英文为 argument)的名字被称为依赖名字(dependent name),C++标准规定,如果解析器在一个模板中遇到一个嵌套依赖名字,它假定那个名字不是一个类型,除非显式用 typename 关键字前置修饰该名字;
和上一条 typename 用法类似,template 用于指明嵌套类型或函数为模板;
this 用于指定查找基类中的成员(当基类是依赖模板参数的类模板实例时,由于实例化总是推迟,这时不依赖模板参数的名字不在基类中查找,文献第 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;
}
global f()
member f(): 99
member f(): 22
global f()