当前位置: 首页 > 编程笔记 >

浅谈C++模板元编程

公孙成仁
2023-03-14
本文向大家介绍浅谈C++模板元编程,包括了浅谈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++面经,都是重基础,能从计算机基础几门课中抽离出自己对于计算机本身的理解就能够得心应手,再结合实际的技术开发,甚至剖析源码也是很好的方式,重点在于看你有没有基本的计算机素养和兴趣,一个重视潜力和能干活的人∽ PS:有想来练练手的,也欢迎投递万兴科技有限公司,多实战积累经验∽ 万兴科技内推码:NTAN8PP #面经##校招##内推#

  • 问题内容: 静态元编程(也称为“模板元编程”)是一种出色的C ++技术,它允许在编译时执行程序。阅读以下规范元编程示例后,一个灯泡突然在我的脑海中闪过: 如果要了解有关C ++静态元编程的更多信息,最好的资源是什么(书籍,网站,在线课件,等等)? 问题答案: [回答我自己的问题] 到目前为止,我发现的最好的介绍是Krzysztof Czarnecki和Ulrich W. Eisenecker撰写的

  • 技术的学习是一个登山的过程。第一章是最为平坦的山脚道路。而从这一章开始,则是正式的爬坡。无论是我写作还是你阅读,都需要付出比第一章更多的代价。那么问题就是,付出更多的精力学习模板是否值得? 这个问题很功利,但是一针见血。因为技术的根本目的在于解决需求。那C++的模板能做什么? 一个高(树)大(新)上(风)的回答是,C++里面的模板,犹如C中的宏、C#和Java中的自省(restropection)

  • 本文向大家介绍浅谈轻量级js模板引擎simplite,包括了浅谈轻量级js模板引擎simplite的使用技巧和注意事项,需要的朋友参考一下 模板地址:https://github.com/zhangshaolong/simplite欢迎各位提出宝贵意见及贡献代码。特点: 1:代码量少,学习成本低; 2:默认jsp语法标签方式,熟悉jsp的朋友可以直接按照jsp的语法书写模板; 3:使用原生js语法

  • 本文向大家介绍浅谈C#设计模式之代理模式,包括了浅谈C#设计模式之代理模式的使用技巧和注意事项,需要的朋友参考一下 代理模式是常用的结构型设计模式之一,当无法直接访问某个对象或访问某个对象存在困难时可以通过一个代理对象来间接访问,为了保证客户端使用的透明性,所访问的真实对象与代理对象需要实现相同的接口.根据代理模式的使用目的不同,代理模式又可以分为多种类型,例如保护代理、远程代理、虚拟代理、缓冲代

  • 本文向大家介绍浅谈JAVA设计模式之享元模式,包括了浅谈JAVA设计模式之享元模式的使用技巧和注意事项,需要的朋友参考一下 享元模式(Flyweight Pattern)主要用于减少创建对象的数量,以减少内存占用和提高性能。这种类型的设计模式属于结构型模式,它提供了减少对象数量从而改善应用所需的对象结构的方式。 享元模式尝试重用现有的同类对象,如果未找到匹配的对象,则创建新对象。我们将通过创建 5