浅谈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)

相关推荐

  • 浅谈C++模板元编程

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

  • 浅谈Python的元编程

    目录 一.装饰器 二.装饰器的执行顺序 三.元类 四.descriptor 类(描述符类) 五.总结 相应的元编程就是描述代码本身的代码,元编程就是关于创建操作源代码(比如修改.生成或包装原来的代码)的函数和类.主要技术是使用装饰器.元类.描述符类. 一.装饰器 装饰器就是函数的函数,它接受一个函数作为参数并返回一个新的函数,在不改变原来函数代码的情况下为其增加新的功能,比如最常用的计时装饰器: from functools import wraps def timeit(logger=None

  • 浅谈Laravel模板实体转义带来的坑

    问题 最近在Laravel项目中用到了百度编辑器,插入到数据库我保存的是原始的html标签代码,没有进行实体转义.然后在修改的时候,需要读取到数据库中的数据,进行回显,这时候竟然在编辑器里面显示html标签代码<p>123</p>,这让我很尴尬,因为以前在tp框架中也是这样写的,但是没有问题. 搜索之路 在知道问题之后,我就开始找百度了,因为一开始的时候我并不知道是框架的原因,我以为是百度编辑器版本的原因,然后收到了许多答案,都是围绕着htmlentities和html_entit

  • 浅谈Java实现面向对象编程java oop

    一.对象的综述 面向对象编程(OOP)具有多方面的吸引力.对管理人员,它实现了更快和更廉价的开发与维护过程.对分析与设计人员,建模处理变得更加简单,能生成清晰.易于维护的设计方案.对程序员,对象模型显得如此高雅和浅显.此外,面向对象工具以及库的巨大威力使编程成为一项更使人愉悦的任务.每个人都可从中获益,至少表面如此. 所有编程语言的最终目的都是解决企业又或者人在现实生活中所遇到的问题,最初我们的程序可能长这样"11111100001",相信大家都不会陌生,只是大家没这么子去敲过代码.再

  • C++11模板元编程-std::enable_if示例详解

    C++11中引入了std::enable_if函数,函数原型如下: template< bool B, class T = void > struct enable_if; 可能的函数实现: template<bool B, class T = void> struct enable_if {}; template<class T> struct enable_if<true, T> { typedef T type; }; 由上可知,只有当第一个模板参数为

  • C++模板元编程实现选择排序

    前言 模板在C++一直是比较神秘的存在. STL 和 Boost 中都有大量运用模板,但是对于普通的程序员来说,模板仅限于使用.在一般的编程中,很少会有需要自己定义模板的情况.但是作为一个有理想的程序员,模板是一个绕不过去的坎.由于C++标准的不断改进,模板的能力越来越强,使用范围也越来越广. 在C++11中,模板增加了 constexpr ,可变模板参数,回返类型后置的函数声明扩展了模板的能力:增加了外部模板加快了模板的编译速度:模板参数的缺省值,角括号和模板别名使模板的定义和使用变得更加的简

  • 浅谈Rx响应式编程

    目录 一.Observable 二.高阶函数 三.快递盒模型 3.1.快递盒模型1:fromEvent 3.2.快递盒模型2:interval 四.高阶快递盒 五.销毁快递盒 5.1.销毁快递盒--取消订阅 5.2.销毁高阶快递盒 六.补充 七.后记 一.Observable Observable从字面翻译来说叫做"可观察者",换言之就是某种"数据源"或者"事件源",这种数据源具有可被观察的能力,这个和你主动去捞数据有本质区别.用一个形象的比喻就

  • <b>浅谈 ASP 模板技术之参数传递</b>

    在内容系统开发中,涉及内容和形式分离的过程,也就是根据用户自定义页面模板然后替换成相关内容的过程.这和外面很多整站的内容管理系统,有本质上的区别.有不少内容管理系统,多少人用,都是一个样子,因为页面无法自定义,不懂编程的用户无法修改.象那种,只填几个参数就出来的网站,我估计是没有什么前途的.因为人人都是一个样子,人人都是会填那些参数的. 举个例子,你查看一下以下几个站点,你会认为他们是一套程序吗?www.blueidea.comhttp://pages.blueidea.comhttp://di

  • 浅谈Java面向接口编程

    我想,对于各位使用面向对象编程语言的程序员来说,"接口"这个名词一定不陌生,但是不知各位有没有这样的疑惑:接口有什么用途?它和抽象类有什么区别?能不能用抽象类代替接口呢?而且,作为程序员,一定经常听到"面向接口编程"这个短语,那么它是什么意思?有什么思想内涵?和面向对象编程是什么关系?本文将一一解答这些疑问. 1.面向接口编程和面向对象编程是什么关系 首先,面向接口编程和面向对象编程并不是平级的,它并不是比面向对象编程更先进的一种独立的编程思想,而是附属于面向对象思

  • 浅谈:什么是数据驱动编程的详解

    前言:最近在学习<Unix编程艺术>.以前粗略的翻过,以为是介绍unix工具的.现在认真的看了下,原来是介绍设计原则的.它的核心就是第一章介绍的unix的哲学以及17个设计原则,而后面的内容就是围绕它来展开的.以前说过,要学习适合自己的资料,而判断是否适合的一个方法就是看你是否能够读得下去.我对这本书有一种相见恨晚的感觉.推荐有4~6年工作经验的朋友可以读一下.正题:作者在介绍Unix设计原则时,其中有一条为"表示原则:把知识叠入数据以求逻辑质朴而健壮".结合之前自己的一些

随机推荐