发表于

用现代C++读C++ Template Metaprogramming

这本书也很有历史了,不过算是系统性讲解模版元编程的第一本书。书中提到的是boost::mpl,实际上对于现代C++已经有了boost::mp11,因此这本书应该和boost::mp11一起看会比较好。

特征

首先是要认识特征,特征模板可以接受参数计算出它的特征出来,因此定义了编译期的类型协议,要求某一系列的类型都要有这些特征。这样特征模板才能发挥出它的能力。我们使用特征的原因是有时候我们总想通过一个类型去获得另一个类型,就不得不用特征模板。典型的比如STL中所有的迭代器都要定义五个特征,然后有个std::iterator_traits特征模板可以计算出迭代器的五个特征出来。

多态

多态的本意是多个不同的对象可以使用相同的函数名称的函数。动态多态要通过指针或引用加上虚函数来实现,而静态多态的主要方法是实现不同的函数签名,因为类型计算可以让函数签名中的类型变化。

函数模板是很特殊的存在,因为不仅仅有模板参数表,还有函数参数表,两者共同决定了函数签名。当然还有一种是通过类模板加上相同的函数签名的成员函数来实现多态。

元函数

元函数是特征模板的一种特例,就是仅有一个特征输出。这样设计的目的是为了减少类型计算的负担,很多时候很多特征但是我们只会用其中一个。值元函数通常以value结尾,类型元函数通常以type结尾。

元函数和普通函数的区别

  • 实现特化,可以类比普通函数中只能使用if/else
  • 多个返回值

C++的编译期计算是函数式编程,元数据都是不变性的。这一章是本书的第二章,后续章节中主要是介绍了标准库中的头文件,在书出版的年代是还属于boost库的。

深入元编程

scientific and engineering c++中第一次实现了量纲分析,书的作者就是BN Trick的两位作者。量纲分析的方法是,首先定义需要进行量纲分析的所有量纲,通过取值为0或者1的一串序列来表示单个量纲,如果量纲和量纲有了乘除关系,则对应的量纲的值会有变化。

template<typename T, typename P>
struct quantity{
  T value;
};

这里的P就是表征量纲。量纲本身其实是自带一个序列的,所以乘除量纲的时候需要两个序列之间对应元素做操作,因此需要用到mp_transform操作。

内嵌模板类实现传递参数,是个很巧妙的方法。

class Wrapper{
  public:
    template<typename T1, typename T2>
    using type = plus<T1,T2>::type
};

实现了将一个特征模板传递给另一个特征模板。或许我们可以叫做特征模板包装类,书中是叫做元函数类。个人认为元函数是特征模板的一种特例。

mp_transform是函数式编程中的高阶函数(high order function),可以用于传参。同样也有lambda可以生成一个特征模板包装类。

包装器

主要就是介绍std::integral_constant的使用,有点像是policy trait的感觉,编译期和运行期都适用。但是觉得自己对于这个特征模板的理解还是不够深刻,这个模板应该是用在什么地方呢?

跨越编译期和运行期的边界

中间有几章被略过了,因为这些其实就是在讲编译期的STL罢了。而且mpl的实现是和mp11不一样的,我觉得应该去直接学习mp11的使用。

这里面的好几章都是看的mp11的官方文档

主模板+偏特化+类型推导

这是参数包操作的最基础的知识,就是对于给定的参数包,如果我们要对其进行操作,就需要通过元函数来实现。而通常元函数都是处理单个类型,而且需要把某个参数包类中的参数表传递给这个元函数。因此需要通过一个类似mp_rename的机制,来传递参数表。

template<class A, template<typename...> class B> class mp_rename_impl;

template<template<typename...> class A,typename... T, template<typename...> class B>
class mp_rename_impl<A<T...>,B>{
  using type = B<T...>;
};

template<class A, template<typename...> class B>
using mp_rename = typename mp_rename_impl<A,B>::type;

这是一个最基础的参数包转移方法,也是整个mp11都最基础方法。

template<typename... T>
using mp_length = std::integral_constant<std::size_t, sizeof...(T)>;

这是装载参数表最基础的方法。我们可以得到

template<typename L>
using mp_size = mp_rename<L, mp_length>;

template<template<typename...> class F, typename L>
using mp_apply = mp_rename<L, F>;

mp_transform

可以支持一元或者二元的元函数。

Implmentation Selection

编译期if,类模板特化,标签分发都是实现方法。

Structure Selection

主要是指如果类模板参数中有类型是空类型,那么应该继承该类型而不是仍然放置一个成员,其实就是编译期决定是否要使用EBCO

函数指针作为形参

通过指向成员函数或者函数的指针,来类型推到模板的形式参数。而且模板的形参类似如下

template<typename T, T t>
class example{};

也就是用第一个的类型去初始化了一个编译期的值。这个在std::integral_constant中也是有使用的。而指向函数的指针非常适合这种形参的声明。

文章中给的例子是将两个函数f和g复合起来,然后通过std::transform的最后一个参数来让一个序列的值都调用这个复合函数fg。这里有一些像是函数式编程的意思在里面。

类型擦除

把不同的类型的区别,通过构造函数模板擦除成了相同的类型。

典型的比如future/thread/function之类的,本来函数的参数表中包含了丰富的类型信息,但是通过对应的promise/bind之类的函数变成了统一的类型。

统一的类型对于编译期计算是不利的,只有编译期计算才能够实现惰性求值。

CRTP

目的是为了解决ADL可能带来的潜在问题,这一部分没有看太懂。但是主要都是用来实现编译期的facet模式

CRTP和友元函数的结合,似乎是一种常见的使用方法。

DSEL

DSL需要满足以下条件

  • 符号系统
  • 基于符号系统的规则
  • 规则构建一个集合

DSL的符号系统不一定要基于文本,也可以是摩尔斯码或者UML。

BNF

BNF(Backus Naur Form)是一种定义DSL的方法。BNF对于人类的理解来说是方便的,保留这种理解的便利,用DSEL来将运行期计算和这种BNF相结合,或者可以理解成设计一条编译期的api,然后通过DSEL来实现这套api。

EBNF在BNF的基础上增加了0个或者多个,1个或者多个,分组括号等功能,这些功能类似于正则表达式中的*+()

YACC

YACC能够将EBNF转换成c/c++代码。

DSEL实现

更高级的规则都是通过低级的规则组合而成的,越是高级的规则使用起来更加方便,这是我们追求DSL的原因。比如我们知道了Make的语法规则,我们可以用C++写一个程序产生Makefile文件,然后再用Make编译;YACC的逻辑是先用EBNF定义规则,然后将部分C++代码中的符号替换,最后YACC生成代码。

DSEL的实现方式是,在C++中嵌入DSL,这个DSL指导了我们如何利用C++实现规则,相当于c++是一个低级规则实现了更高规则的DSL,则称这种DSL是DSEL。

C++实现DSL的前提

C++有很多特点

  • 静态类型系统
  • 零成本抽象
  • 模板系统实现编译期计算
  • 预处理器
  • 内建运算符

表达式模板

一个表达式是一组规则的具体实现,我们要等到赋值运算符出现的时候再进行真正的计算,这样可以消除中间值的存取过程。扩展一点说,EBNF定义的规则,都可以考虑进行惰性求值。

  • 通过类模板和编译期计算不断生成DSEL需求的类
  • 生成的类型必须支持一个contexpr的函数,可以实现编译期展开
  • 生成的类型必须对操作数据进行引用或者指针指向
  • 类模板生成的所有类型通过递归调用,展开了constexpr函数实现惰性求值,避免了求中间值,优化了代码

表达式模板的一个例子

这里关键是把L和R的[]运算符和OpTag的[]运算符统一起来了。

template <typename L, typename OpTag, typename R>
class Expression{
 public:
  Expression(L const& l, R const& r): l(l),r(r){}
  auto operator[](uint64_t index) const{
    return OpTag::Apply(l[index], r[index]);
  }

 public:
  L const& l;
  R const& r;
};

实现的原理是将operator []索引操作符作先存储到了Expression这个对象中,操作对象的引用存储下来了,操作过程通过operator []索引操作符来定义。然后通过不断生成新的Expression,直到最后调用赋值运算的时候,调用该operator []索引操作符进行计算。

表达式模板的中间结果

auto关键字的重启原因是表达式模版中,如果需要中间结果时,中间结果的类型是很复杂的,手写非常的耗费心智。

DSEL理解

书中列举的很多库的例子似乎都在说明DSEL很接近函数式编程,比如boost::bind,boost::lambda,boost::spirit。

C++中就是不断生成支持operator xxx运算符的类模板实例化类型,所有的操作符都是constexpr的,进而进行类型推导,生成一个新的类模板实例化类型。

C++中实现DSEL是在不断生成新的模板类,最终生成的模板类实例化后生成一个类型,运行期直接调用该类型的函数实现了惰性求值。EBNF规则也是类似的,一个文法也是通过不断用规则来替换非终端符号,最终完全用终端符号来表示的过程。

FSM实现

一个状态机有三种抽象:状态,事件,切换。书中介绍了,如何将DSEL转换成C++中的语法来实现

编译期遍历STT生成了一个状态机

template< typename Transition, typename Next>
class event_dispatcher{
  static int dispatch(fsm_t& fsm, int state, event const& e){
    if (state == Transition::Current_State){
        Transition::Execute(fsm, e);
        return Transition::Next_State;
    } else {
        return Next::dispatch(fsm, state, e);
    }
  }
};