C++模板元编程

来源:互联网 发布:万网域名可以不备案吗 编辑:程序博客网 时间:2024/05/20 10:21

C++模板元编程

http://www.mincoder.com/article/4403.shtml

所谓元编程就是编写直接生成或操纵程序的程序,C++ 模板给 C++ 语言提供了元编程的能力,模板使 C++ 编程变得异常灵活,能实现很多高级动态语言才有的特性(语法上可能比较丑陋,一些历史原因见下文)。普通用户对 C++ 模板的使用可能不是很频繁,大致限于泛型编程,但一些系统级的代码,尤其是对通用性、性能要求极高的基础库(如 STL、Boost)几乎不可避免的都大量地使用 C++ 模板,一个稍有规模的大量使用模板的程序,不可避免的要涉及元编程(如类型计算)。本文就是要剖析 C++ 模板元编程的机制。

下面所给的所有代码,想做实验又懒得打开编译工具?一个在线运行 C++ 代码的网站(GCC 4.8)很好~

  1. C++模板的语法

函数模板(function template)和类模板(class template)的简单示例如下:

include

// 函数模板
template
bool equivalent(const T& a, const T& b){
return !(a < b) && !(b < a);
}
// 类模板
template // 默认参数
class bignumber{
T _v;
public:
bignumber(T a) : _v(a) { }
inline bool operator<(const bignumber& b) const; // 等价于 (const bignumber b)
};
// 在类模板外实现成员函数
template
bool bignumber::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(1, 2) << ‘\n’;
std::cin.get(); return 0;
}

程序输出如下:

1
0

关于模板(函数模板、类模板)的模板参数(详见文献[1]第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

include

// 识别两个类型是否相同,提前进入模板元编程^_^
template

include

template
class aTMP {
public:
void f1() { std::cout << “f1()\n”; }
void f2() { std::ccccout << “f2()\n”; } // 敲错键盘了,语义错误:没有 std::ccccout
};
int main(){
aTMP a;
a.f1();
// a.f2(); // 这句代码被注释时,aTMP::f2() 不被实例化,从而上面的错误被掩盖!
std::cin.get(); return 0;
}

所以模板代码写完后最好写个诸如显示实例化的测试代码,更深入一些,可以插入一些模板调用代码使得编译器及时发现错误,而不至于报出无限长的错误信息。另一个例子如下(GCC 4.8 下编译的输出信息,VS2013 编译输出了 500 多行错误信息):

include

// 计算 N 的阶乘 N!
template
class aTMP{
public:
enum { ret = N==0 ? 1 : N * aTMP::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::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==0,而仅仅知道其依赖 aTMP(lazy instantiation),从而产生无限递归,纠正方法是使用模板特例化,如下:

include

// 计算 N 的阶乘 N!
template
class aTMP{
public:
enum { ret = N * aTMP::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]模板):

包含模板编译模式:编译器生成每个编译单元中遇到的所有的模板实例,并存放在相应的目标文件中;链接器合并等价的模板实例,生成可执行文件,要求实例化时模板定义可见,不能使用系统链接器;分离模板编译模式(使用 export 关键字):不重复生成模板实例,编译器设计要求高,可以使用系统链接器;包含编译模式是主流,C++11 已经弃用 export 关键字(对模板引入 extern 新用法),一般将模板的全部实现代码放在同一个头文件中并在用到模板的地方用 #include 包含头文件,以防止出现实例不一致(如下面紧接着例子);

实例化,编译链接的简单例子如下(参考了文献[1]第10页):

// file: a.cpp

include

template
class MyClass { };
template MyClass::MyClass(); // 显示实例化构造函数 MyClass::MyClass()
template class MyClass; // 显示实例化整个类 MyClass
template
void print(T const& m) { std::cout << “a.cpp: ” << m << ‘\n’; }
void fa() {
print(1); // print,隐式实例化
print(0.1); // print
}
void fb(); // fb() 在 b.cpp 中定义,此处声明
int main(){
fa();
fb();
std::cin.get(); return 0;
}

// file: b.cpp

include

template
void print(T const& m) { std::cout << “b.cpp: ” << m << ‘\n’; }
void fb() {
print(‘2’); // print
print(0.1); // print
}

a.cpp: 1
a.cpp: 0.1
b.cpp: 2
a.cpp: 0.1

上例中,由于 a.cpp 和 b.cpp 中的 print 实例等价(模板实例的二进制代码在编译生成的对象文件 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

template
class aTMP{
public: typedef const T reType;
};
void f() { std::cout << “global f()\n”; }
template
class Base {
public:
template
void f() { std::cout << “member f(): ” << N << ‘\n’; }
};
template
class Derived : public Base {
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::template f<22>(); } // 强制基类 f<22>()
void df4() { ::f(); } // 强制全局 f()
};
int main(){
Derived

include

template

ifndef LAST

define LAST 10

endif

int main() {
Prime_print a; a.f(); // 必须调用 a.f() 以实例化 Prime_print::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::D(void*) [with int i = 2]’ [-fpermissive]
template struct D { D(void*); operator int(); };
^
main.cpp: In instantiation of ‘void Prime_print::f() [with int i = 7]’:
main.cpp:13:36: recursively required from ‘void Prime_print::f() [with int i = 9]’
main.cpp:13:36: required from ‘void Prime_print::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 d = prim ? 1 : 0; a.f(); }
^
main.cpp:2:28: warning: initializing argument 1 of ‘D::D(void*) [with int i = 7]’ [-fpermissive]
template struct D { D(void*); operator int(); };
^
main.cpp: In instantiation of ‘void Prime_print::f() [with int i = 5]’:
main.cpp:13:36: recursively required from ‘void Prime_print::f() [with int i = 9]’
main.cpp:13:36: required from ‘void Prime_print::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 d = prim ? 1 : 0; a.f(); }
^
main.cpp:2:28: warning: initializing argument 1 of ‘D::D(void*) [with int i = 5]’ [-fpermissive]
template struct D { D(void*); operator int(); };
^
main.cpp: In instantiation of ‘void Prime_print::f() [with int i = 3]’:
main.cpp:13:36: recursively required from ‘void Prime_print::f() [with int i = 9]’
main.cpp:13:36: required from ‘void Prime_print::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 d = prim ? 1 : 0; a.f(); }
^
main.cpp:2:28: warning: initializing argument 1 of ‘D::D(void*) [with int i = 3]’ [-fpermissive]
template struct D { D(void*); operator int(); };

上面的编译输出信息只给出了前一部分,虽然信息很杂,但还是可以看到其中有 10 以内全部素数:2、3、5、7(已经加粗显示关键行)。

到目前为止,虽然已经看到了阶乘、求和等递归数值计算,但都没涉及原理,下面以求和为例讲解 C++ 模板编译期数值计算的原理:

include

template
class sumt{
public: static const int ret = sumt::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 。函数式编程看上去似乎效率低下(因为它和数学接近,而不是和硬件工作方式接近),但有自己的优势:描述问题更加简洁清晰(前提是熟悉这种方式),没有可变的变量就没有数据依赖,方便进行并行化。

  1. 模板下的控制结构

模板实现的条件 if 和 while 语句如下(文献[9]):

// 通例为空,若不匹配特例将报错,很好的调试手段(这里是 bool 就无所谓了)
template

include // std::swap

// dynamic code, 普通函数版本
void bubbleSort(int* data, int n)
{
for(int i=n-1; i>0; –i) {
for(int j=0; j

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

include

include

include

include

include // std::swap

// 整合成一个类模板实现,看着好,但引入了 代码膨胀
template
class IntBubbleSortC {
template

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

include // std::swap

// static code, 模板元编程版本
template

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。可以看到,类模板封装版的对象文件不但绝对大小更大,而且增长更快,这和之前分析是一致的。

  1. 表达式模板,向量运算

文献[12]展示了一个表达式模板(Expression Templates)的例子:

include // std::cout

include // 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

include // std::cout

include // std::sqrt()

void evaluate(double start, double end, double step) {
double _temp = 1.0;
for(double i=start; i

include // std::cout

// A CRTP base class for Vecs with a size and indexing:
template
class VecExpr {
public:
double operator[](int i) const { return static_cast

include // std::cout

include

template
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 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
class mytraits{
public: typedef typename iter::value_type value_type;
};
template
class mytraits

include

include

template

include // std::cout

// whether T could be converted to U
template

include

include

// thanks to Substitution failure is not an erro (SFINAE)
template
struct has_typedef_value_type {
typedef char Type1[1];
typedef char Type2[2];
template static Type1& test(typename C::value_type*);
template static Type2& test(…);
public:
static const bool ret = sizeof(test(0)) == sizeof(Type1); // 0 == NULL
};
struct foo { typedef float lalala; };
int main() {
std::cout << has_typedef_value_type

include

class null_type {}; // 标签类,标记参数列表末尾
template

include

// 元容器
template
class meta_container {
public:
typedef T0 type;
typedef meta_container

0 0
原创粉丝点击