C++模板元编程中的代数数据类型(ADT)及模式匹配等操作:以编译期AVL树为例
前言
众所周知C++是可以在编译期进行用户可控的图灵完全计算的,其方法主要是利用模板元编程(TMP),以及在C++11出现并在C++14和C++17中不断增强的constexpr
相关的语法(关于这个,clang的源码中有个很有意思的测试样例:a href=”https://github.com/llvm-mirror/clang/blob/master/test/SemaCXX/constexpr-turing.cpp“>用constexpr实现编译期图灵机)。其中constexpr
在使用上会更加简洁,而TMP写起来则繁杂些。不过TMP在形式上支持一些比较先进的PL原语,比如模式匹配,这是单纯用constexpr
做不到的。
前几天学习OCaml的时候用OCaml实现了一个函数式AVL树,事后想了想好像用C++的TMP也可以实现个类似的编译期AVL树,试着做了一下,发现将OCaml中的ADT以及基于模式匹配的对于ADT的操作转化到TMP竟然出乎意料地顺滑。于是便写此文章,以编译期AVL树为例,展现一下用TMP实现ADT的简易性。
关于AVL树的定义与操作,如有需要请自行搜索,本文不提供详细说明。
AVL树定义
首先是定义AVL树的ADT。
1 |
|
其中avl_node
是AvlNode的构造器,avl_height
是avl_tree
的树高属性的萃取器。
转化为C++ TMP代码:
1 |
|
其中用继承来表现variant。
这里和OCaml的实现有点不同,将树高作为类的静态常量保存,不过无伤大雅。
在avl_node
的构造中会对模板参数L
和R
进行检查,保证其为avl_tree
。
AVL树操作
元素查询
OCaml的代码非常直接:
1 |
|
可以用模板偏特化来实现模式匹配。其中:
- 类模板的基础定义可以表示模式匹配中的缺省情况(如下面的
static constexpr bool value = false;
)。 - 对模板的不同形式的偏特化可以表示模式匹配的不同模式。
1 |
|
其中avl_operation
主要用于检查模板参数是否为avl_tree
:
1 |
|
旋转调整
针对AVL树四种需要旋转的模式进行调整:
1 |
|
OCaml的代码中用到了模式匹配的when
子句,这个可用std::enable_if
来模拟:
1 |
|
该段代码可以很好地展现C++ TMP对模式匹配的支持。
插入元素
OCaml代码:
1 |
|
注意多段的条件判断单纯使用C++ TMP不是很好实现,因为std::conditional
会对两个分支都求值,而直接用模板匹配来实现这么多个分支又太过繁琐。因此借助了一些constexpr
相关语法的帮助,让实现更加简洁:
1 |
|
最大值/最小值
实现非常简单,主要是为删除元素做铺垫。
OCaml实现:
1 |
|
C++实现:
1 |
|
删除元素
直接上代码吧……
OCaml代码:
1 |
|
C++代码:
1 |
|
其中用利用constexpr
变量可以实现let ... in ...
这样的局部绑定。
简单测试
OCaml部分
先定义打印AVL树的函数:
1 |
|
之后构造简单的测试样例:先按顺序插入1~10,之后删除1,3,5。
1 |
|
输出结果:
1 |
|
C++部分
定义打印函数,以及用于构造连续插入1~n的AVL树的元函数:
1 |
|
之后构造简单测试:插入1~10,之后删除1,3,5:
1 |
|
打印结果和OCaml的一样(废话……):
1 |
|
后记
本文旨在结合ADT、模式匹配等比较先进的PL概念,提供一些定义C++编译期数据结构的思路和较为简洁的写法。至于编译期AVL树则纯属玩具(毕竟对编译时间的影响还是挺大的,而且一般编译器的模板运算递归深度顶多一两千),看着图一乐就行了。