C++模板元编程(C++ template metaprogramming)

[复制链接]
10033|89
 楼主| elecintop 发表于 2015-3-24 20:28 | 显示全部楼层
C++11 关于模板的新特性(详见文献[1]第15章,文献[4]C++11):

“>>” 根据上下文自动识别正确语义;
函数模板参数默认值;
变长模板参数(扩展 sizeof…() 获取参数个数);
模板别名(扩展 using 关键字);
外部模板实例(拓展 extern 关键字),弃用 export template。
在本文中,如无特别声明将不使用 C++11 的特性(除了 “>>”)。
 楼主| elecintop 发表于 2015-3-24 20:28 | 显示全部楼层
2. 模板元编程概述

如果对 C++ 模板不熟悉(光熟悉语法还不算熟悉),可以先跳过本节,往下看完例子再回来。

C++ 模板最初是为实现泛型编程设计的,但人们发现模板的能力远远不止于那些设计的功能。一个重要的理论结论就是:C++ 模板是图灵完备的(Turing-complete),其证明过程请见文献[8](就是用 C++ 模板模拟图灵机),理论上说 C++ 模板可以执行任何计算任务,但实际上因为模板是编译期计算,其能力受到具体编译器实现的限制(如递归嵌套深度,C++11 要求至少 1024,C++98 要求至少 17)。C++ 模板元编程是“意外”功能,而不是设计的功能,这也是 C++ 模板元编程语法丑陋的根源。
 楼主| elecintop 发表于 2015-3-24 20:29 | 显示全部楼层
C++ 模板是图灵完备的,这使得 C++ 成为两层次语言(two-level languages,中文暂且这么翻译,文献[9]),其中,执行编译计算的代码称为静态代码(static code),执行运行期计算的代码称为动态代码(dynamic code),C++ 的静态代码由模板实现(预处理的宏也算是能进行部分静态计算吧,也就是能进行部分元编程,称为宏元编程,见 Boost 元编程库即 BCCL,文献[16]和文献[1] 10.4)。
 楼主| elecintop 发表于 2015-3-24 20:29 | 显示全部楼层
具体来说 C++ 模板可以做以下事情:编译期数值计算、类型计算、代码计算(如循环展开),其中数值计算实际不太有意义,而类型计算和代码计算可以使得代码更加通用,更加易用,性能更好(也更难阅读,更难调试,有时也会有代码膨胀问题)。编译期计算在编译过程中的位置请见下图(取自文献[10]),可以看到关键是模板的机制在编译具体代码(模板实例)前执行:
 楼主| elecintop 发表于 2015-3-24 20:29 | 显示全部楼层

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?注册

×
 楼主| elecintop 发表于 2015-3-24 20:30 | 显示全部楼层
从编程范型(programming paradigm)上来说,C++ 模板是函数式编程(functional programming),它的主要特点是:函数调用不产生任何副作用(没有可变的存储),用递归形式实现循环结构的功能。C++ 模板的特例化提供了条件判断能力,而模板递归嵌套提供了循环的能力,这两点使得其具有和普通语言一样通用的能力(图灵完备性)。
 楼主| elecintop 发表于 2015-3-24 20:30 | 显示全部楼层
从编程形式来看,模板的“<>”中的模板参数相当于函数调用的输入参数,模板中的 typedef 或 static const 或 enum 定义函数返回值(类型或数值,数值仅支持整型,如果需要可以通过编码计算浮点数),代码计算是通过类型计算进而选择类型的函数实现的(C++ 属于静态类型语言,编译器对类型的操控能力很强)。
 楼主| elecintop 发表于 2015-3-24 20:31 | 显示全部楼层
代码示意如下:
  1. #include <iostream>

  2. template<typename T, int i=1>
  3. class someComputing {
  4. public:
  5.     typedef volatile T* retType; // 类型计算
  6.     enum { retValume = i + someComputing<T, i-1>::retValume }; // 数值计算,递归
  7.     static void f() { std::cout << "someComputing: i=" << i << '\n'; }
  8. };
  9. template<typename T> // 模板特例,递归终止条件
  10. class someComputing<T, 0> {
  11. public:
  12.     enum { retValume = 0 };
  13. };

  14. template<typename T>
  15. class codeComputing {
  16. public:
  17.     static void f() { T::f(); } // 根据类型调用函数,代码计算
  18. };

  19. int main(){
  20.     someComputing<int>::retType a=0;
  21.     std::cout << sizeof(a) << '\n'; // 64-bit 程序指针
  22.     // VS2013 默认最大递归深度500,GCC4.8 默认最大递归深度900(-ftemplate-depth=n)
  23.     std::cout << someComputing<int, 500>::retValume << '\n'; // 1+2+...+500
  24.     codeComputing<someComputing<int, 99>>::f();
  25.     std::cin.get(); return 0;
  26. }

  1. 8
  2. 125250
  3. someComputing: i=99
 楼主| elecintop 发表于 2015-3-24 20:31 | 显示全部楼层
C++ 模板元编程概览框图如下(取自文献[9]):

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?注册

×
 楼主| elecintop 发表于 2015-3-24 20:32 | 显示全部楼层
3. 编译期数值计算

第一个 C++ 模板元程序是 Erwin Unruh 在 1994 年写的(文献[14]),这个程序计算小于给定数 N 的全部素数(又叫质数),程序并不运行(都不能通过编译),而是让编译器在错误信息中显示结果(直观展现了是编译期计算结果,C++ 模板元编程不是设计的功能,更像是在戏弄编译器,当然 C++11 有所改变),由于年代久远,原来的程序用现在的编译器已经不能编译了
 楼主| elecintop 发表于 2015-3-24 20:32 | 显示全部楼层
下面的代码在原来程序基础上稍作了修改(GCC 4.8 下使用 -fpermissvie,只显示警告信息):
  1. // Prime number computation by Erwin Unruh
  2. template<int i> struct D { D(void*); operator int(); }; // 构造函数参数为 void* 指针

  3. template<int p, int i> struct is_prime { // 判断 p 是否为素数,即 p 不能整除 2...p-1
  4.     enum { prim = (p%i) && is_prime<(i>2?p:0), i-1>::prim };
  5. };
  6. template<> struct is_prime<0, 0> { enum { prim = 1 }; };
  7. template<> struct is_prime<0, 1> { enum { prim = 1 }; };

  8. template<int i> struct Prime_print {
  9.     Prime_print<i-1> a;
  10.     enum { prim = is_prime<i, i-1>::prim };
  11.     // prim 为真时, prim?1:0 为 1,int 到 D<i> 转换报错;假时, 0 为 NULL 指针不报错
  12.     void f() { D<i> d = prim?1:0; a.f(); } // 调用 a.f() 实例化 Prime_print<i-1>::f()
  13. };
  14. template<> struct Prime_print<2> { // 特例,递归终止
  15.     enum { prim = 1 };
  16.     void f() { D<2> d = prim?1:0; }
  17. };

  18. #ifndef LAST
  19. #define LAST 10
  20. #endif

  21. int main() {
  22.     Prime_print<LAST> a; a.f(); // 必须调用 a.f() 以实例化 Prime_print<LAST>::f()
  23. }


 楼主| elecintop 发表于 2015-3-24 21:11 | 显示全部楼层
上面的编译输出信息只给出了前一部分,虽然信息很杂,但还是可以看到其中有 10 以内全部素数:2、3、5、7(已经加粗显示关键行)。
 楼主| elecintop 发表于 2015-3-24 21:12 | 显示全部楼层
到目前为止,虽然已经看到了阶乘、求和等递归数值计算,但都没涉及原理,下面以求和为例讲解 C++ 模板编译期数值计算的原理:
  1. #include <iostream>

  2. template<int N>
  3. class sumt{
  4. public: static const int ret = sumt<N-1>::ret + N;
  5. };
  6. template<>
  7. class sumt<0>{
  8. public: static const int ret = 0;
  9. };

  10. int main() {
  11.     std::cout << sumt<5>::ret << '\n';
  12.     std::cin.get(); return 0;
  13. }
 楼主| elecintop 发表于 2015-3-24 21:12 | 显示全部楼层
当编译器遇到 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 个类型。
 楼主| elecintop 发表于 2015-3-24 21:13 | 显示全部楼层
从这个例子我们也可以窥探 C++ 模板元编程的函数式编程范型,对比结构化求和程序:for(i=0,sum=0; i<=N; ++i) sum+=i; 用逐步改变存储(即变量 sum)的方式来对计算过程进行编程,模板元程序没有可变的存储(都是编译期常量,是不可变的变量),要表达求和过程就要用很多个常量:sumt<0>::ret,sumt<1>::ret,…,sumt<5>::ret 。函数式编程看上去似乎效率低下(因为它和数学接近,而不是和硬件工作方式接近),但有自己的优势:描述问题更加简洁清晰(前提是熟悉这种方式),没有可变的变量就没有数据依赖,方便进行并行化。
 楼主| elecintop 发表于 2015-3-24 21:13 | 显示全部楼层
4. 模板下的控制结构

模板实现的条件 if 和 while 语句如下(文献[9]):
  1. // 通例为空,若不匹配特例将报错,很好的调试手段(这里是 bool 就无所谓了)
  2. template<bool c, typename Then, typename Else> class IF_ { };
  3. template<typename Then, typename Else>
  4. class IF_<true, Then, Else> { public: typedef Then reType; };
  5. template<typename Then, typename Else>
  6. class IF_<false,Then, Else> { public: typedef Else reType; };

  7. // 隐含要求: Condition 返回值 ret,Statement 有类型 Next
  8. template<template<typename> class Condition, typename Statement>
  9. class WHILE_ {
  10.     template<typename Statement> class STOP { public: typedef Statement reType; };
  11. public:
  12.     typedef typename
  13.         IF_<Condition<Statement>::ret,
  14.         WHILE_<Condition, typename Statement::Next>,
  15.         STOP<Statement>>::reType::reType
  16.     reType;
  17. };
 楼主| elecintop 发表于 2015-3-24 21:14 | 显示全部楼层
IF_<> 的使用示例见下面:
  1. const int len = 4;
  2. typedef
  3.     IF_<sizeof(short)==len, short,
  4.     IF_<sizeof(int)==len, int,
  5.     IF_<sizeof(long)==len, long,
  6.     IF_<sizeof(long long)==len, long long,
  7.     void>::reType>::reType>::reType>::reType
  8. int_my; // 定义一个指定字节数的类型
  9. std::cout << sizeof(int_my) << '\n';

 楼主| elecintop 发表于 2015-3-24 21:15 | 显示全部楼层
WHILE_<> 的使用示例见下面:
  1. // 计算 1^e+2^e+...+n^e
  2. template<int n, int e>
  3. class sum_pow {
  4.     template<int i, int e> class pow_e{ public: enum{ ret=i*pow_e<i,e-1>::ret }; };
  5.     template<int i> class pow_e<i,0>{ public: enum{ ret=1 }; };
  6.     // 计算 i^e,嵌套类使得能够定义嵌套模板元函数,private 访问控制隐藏实现细节
  7.     template<int i> class pow{ public: enum{ ret=pow_e<i,e>::ret }; };
  8.     template<typename stat>
  9.     class cond { public: enum{ ret=(stat::ri<=n) }; };
  10.     template<int i, int sum>
  11.     class stat { public: typedef stat<i+1, sum+pow<i>::ret> Next;
  12.                          enum{ ri=i, ret=sum }; };
  13. public:
  14.     enum{ ret = WHILE_<cond, stat<1,0>>::reType::ret };
  15. };

  16. int main() {
  17.     std::cout << sum_pow<10, 2>::ret << '\n';
  18.     std::cin.get(); return 0;
  19. }

 楼主| elecintop 发表于 2015-3-24 21:15 | 显示全部楼层
为了展现编译期数值计算的强大能力,下面是一个更复杂的计算:最大公约数(Greatest Common Divisor,GCD)和最小公倍数(Lowest Common Multiple,LCM),经典的辗转相除算法:
  1. int lcm(int a, int b){
  2.     int r, lcm=a*b;
  3.     while(r=a%b) { a = b; b = r; } // 因为用可变的存储,不能写成 a=b; b=a%b;
  4.     return lcm/b;
  5. }
  6. // 递归函数版本
  7. int gcd_r(int a, int b) { return b==0 ? a : gcd_r(b, a%b); } // 简洁
  8. int lcm_r(int a, int b) { return a * b / gcd_r(a,b); }

  9. // 模板版本
  10. template<int a, int b>
  11. class lcm_T{
  12.     template<typename stat>
  13.     class cond { public: enum{ ret=(stat::div!=0) }; };
  14.     template<int a, int b>
  15.     class stat { public: typedef stat<b, a%b> Next; enum{ div=a%b, ret=b }; };
  16.     static const int gcd = WHILE_<cond, stat<a,b>>::reType::ret;
  17. public:
  18.     static const int ret = a * b / gcd;
  19. };
  20. // 递归模板版本
  21. template<int a, int b>
  22. class lcm_T_r{
  23.     template<int a, int b> class gcd { public: enum{ ret = gcd<b,a%b>::ret }; };
  24.     template<int a> class gcd<a, 0> { public: enum{ ret = a }; };
  25. public:
  26.     static const int ret = a * b / gcd<a,b>::ret;
  27. };

  28. int main() {
  29.     std::cout << lcm(100, 36) << '\n';
  30.     std::cout << lcm_r(100, 36) << '\n';
  31.     std::cout << lcm_T<100, 36>::ret << '\n';
  32.     std::cout << lcm_T_r<100, 36>::ret << '\n';
  33.     std::cin.get(); return 0;
  34. }

  1. 900
  2. 900
  3. 900
  4. 900
 楼主| elecintop 发表于 2015-3-24 21:16 | 显示全部楼层
上面例子中,定义一个类的整型常量,可以用 enum,也可以用 static const int,需要注意的是 enum 定义的常量的字节数不会超过 sizeof(int) (文献[2])。
您需要登录后才可以回帖 登录 | 注册

本版积分规则

快速回复 在线客服 返回列表 返回顶部