欢迎来到入门教程网!

C语言

当前位置:主页 > 软件编程 > C语言 >

浅谈C++模板元编程

来源:本站原创|时间:2020-01-10|栏目: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++库为了提高关键代码的性能,千万要适可而止,要不然止小心被打。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

上一篇:C++二叉树实现词频分析功能

栏    目:C语言

下一篇:C/C++中接收return返回来的数组元素方法示例

本文标题:浅谈C++模板元编程

本文地址:https://www.xiuzhanwang.com/a1/Cyuyan/1012.html

网页制作CMS教程网络编程软件编程脚本语言数据库服务器

如果侵犯了您的权利,请与我们联系,我们将在24小时内进行处理、任何非本站因素导致的法律后果,本站均不负任何责任。

联系QQ:835971066 | 邮箱:835971066#qq.com(#换成@)

Copyright © 2002-2020 脚本教程网 版权所有