C++模板元编程

    所谓元编程就是编写直接生成或操纵程序的程序,C++ 模板给 C++ 语言提供了元编程的能力,模板使 C++ 编程变得异常灵活,能实现很多高级动态语言才有的特性(语法上可能比较丑陋,一些历史原因见下文)。模板元编程的根在模板。模板的使命很简单:为自动代码生成提供方便。提高程序员生产率的一个非常有效的方法就是“代码复用”,而面向对象很重要的一个贡献就是通过内部紧耦合和外部松耦合将“思想”转化成一个一个容易复用的“概念”。但是面向对象提供的工具箱里面所包含的继承,组合与多态并不能完全满足实际编程中对于代码复用的全部要求,于是模板就应运而生了。

   模板是更智能的宏。模板和宏都是编译前代码生成,像宏一样,模板代码会被编译器在编译的第一阶段(在内部转,这点儿与预编译器不同)就展开成合法的C++代码,然后根据展开的代码生成目标代码,链接到最终的应用程序之中。模板与宏相比,它站在更高的抽象层上面,宏操作的是字符串中的token,然而模板却能够操作C++中的类型。所以模板更加安全(因为有类型检查),更加智能(可以根据上下文自动特化)……说完模板,来说说模板元编程。模板元编程其实就是复杂点儿的模板,简单的模板在特化时基本只包含类型的查找与替换,这种模板可以看作是“类型安全的宏”。而模板元编程就是将一些通常编程时才有的概念比如:递归,分支等加入到模板特化过程中的模板,但其实说白了还是模板,自动代码生成而已。普通用户对 C++ 模板的使用可能不是很频繁,大致限于泛型编程,但一些系统级的代码,尤其是对通用性、性能要求极高的基础库(如 STL、Boost)几乎不可避免的都大量地使用 C++ 模板,一个稍有规模的大量使用模板的程序,不可避免的要涉及元编程(如类型计算)。本文就是要剖析 C++ 模板元编程的机制。

   C++ 模板是图灵完备的,这使得 C++ 成为两层次语言(two-level languages,中文暂且这么翻译,文献[9]),其中,执行编译计算的代码称为静态代码(static code),执行运行期计算的代码称为动态代码(dynamic code),C++ 的静态代码由模板实现(预处理的宏也算是能进行部分静态计算吧,也就是能进行部分元编程,称为宏元编程,见 Boost 元编程库即 BCCL,具体来说 C++ 模板可以做以下事情:编译期数值计算、类型计算、代码计算(如循环展开),其中数值计算实际不太有意义,而类型计算和代码计算可以使得代码更加通用,更加易用,性能更好(但是也会让代码也更难阅读,更难调试,有时也会有代码膨胀问题)。总的来说模板元编程的优势在于:

  1.以编译耗时为代价换来卓越的运行期性能(一般用于为性能要求严格的数值计算换取更高的性能)。通常来说,一个有意义的程序的运行次数(或服役时间)总是远远超过编译次数(或编译时间)。
  2.提供编译期类型计算,通常这才是模板元编程大放异彩的地方。
模板元编程技术并非都是优点:
  1.代码可读性差,以类模板的方式描述算法也许有点抽象。
  2.调试困难,元程序执行于编译期,没有用于单步跟踪元程序执行的调试器(用于设置断点、察看数据等)。程序员可做的只能是等待编译过程失败,然后人工破译编译器倾泻到屏幕上的错误信息。
  3.编译时间长,通常带有模板元程序的程序生成的代码尺寸要比普通程序的大,
  4.可移植性较差,对于模板元编程使用的高级模板特性,不同的编译器的支持度不同。

   编译期计算在编译过程中的位置请见下图,可以看到关键是模板的机制在编译具体代码(模板实例)前执行:

   

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

   示例: 

#include <iostream>

template<typename T, int i = 1>
class CComputeSomething {
public:
    typedef volatile T *retType; // 类型计算
    enum {
        retValume = i + CComputeSomething<T, i - 1>::retValume
    }; // 数值计算,递归
    static void f() {
        std::cout << "CComputeSomething:i = " << i << " retValume = " << retValume << '\n';
    }
};

//递归结束特例
template<typename T>
class CComputeSomething<T, 0> {
public:
    enum {
        retValume = 0
    };
};

// 根据类型调用函数,代码计算
template<typename T>
class CComputingFunc {
public:
    static void f() { T::f(); }
};

int main() {
    CComputeSomething<int>::retType a = 0;
    //这里的递归深度注意,不同编译器允许的最大深度不同,编译时添加 -ftemplate-depth=500来修改编译器允许的递归最大深度
    CComputingFunc<CComputeSomething<int, 500>>::f();
    return 0;
}

 C++ 模板元编程概览框图如下:

 

   

编译期数值计算
第一个 C++ 模板元程序是 Erwin Unruh 在 1994 年写的,这个程序计算小于给定数 N 的全部素数(又叫质数),程序并不运行(都不能通过编译),而是让编译器在错误信息中显示结果(直观展现了是编译期计算结果,C++ 模板元编程不是设计的功能,更像是在戏弄编译器,当然 C++11 有所改变,下面以求和为例讲解 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';
    return 0;
}
   当编译器遇到 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 。函数式编程看上去似乎效率低下(因为它和数学接近,而不是和硬件工作方式接近),但有自己的优势:描述问题更加简洁清晰(前提是熟悉这种方式),没有可变的变量就没有数据依赖,方便进行并行化。

模板实现的条件 if 和 while  :

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;
};
模板循环展开  

  模板元编程实现的循环展开能够达到和手动循环展开相近的性能(90% 以上),并且性能是循环版本的 2 倍多(如果扣除 memcpy 函数占据的部分加速比将更高,根据 Amdahl 定律)。这里可能有人会想,既然循环次数固定,为什么不直接手动循环展开呢,难道就为了使用模板吗?当然不是,有时候循环次数确实是编译期固定值,但对用户并不是固定的,比如要实现数学上向量计算的类,因为可能是 2、3、4 维,所以写成模板,把维度作为 int 型模板参数,这时因为不知道具体是几维的也就不得不用循环,不过因为维度信息在模板实例化时是编译期常量且较小,所以编译器很可能在代码优化时进行循环展开。

    我们说过模板元编程实际上就是一些复杂的模板,虽然可以把一些复杂的运算提前到编译器但是代码阅读性极差,如果你不是写一些通用的大型的c++库为了提高关键代码的性能,千万要适可而止,要不然止小心被打,更多的例子和应用看此文章吧:点击打开链接

  

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值