前言 本系列分为上中下三个篇章,前四章作为上篇,详细介绍了宏在产生式编程中的原理和应用,本章开始步入中篇,正式进入 C++ 的产生式元编程技术。 宏是 C 时期的产物,功能颇为简陋,亦非图灵完备,即或诸般奇技妙诀加身,限制仍多。且调试不易,有所束缚,以是仅作辅助,用来实现简单的代码生成功能。至 C++ 时期,产生式元编程工具陡增,其中以模板为一切的根基,发枝散叶,尔今正向静态反射前进。 本章涉及不计其数的模板技术,主要集中于模板的核心理论和用法,概念错综复杂,技术层出不穷。难度等级绝对不低,是本系列中非常重要的一章。 纵然某些概念和技术你已知悉,也莫要跳过某个小节,因为本系列的重心不在模板编程,而在产生式编程,论述角度将有所差异。 泛型……元编程 传统过程式编程的思路是将逻辑作为函数,数据作为函数的输入和输出,通过无数个函数来组织完整的逻辑需求,好似搭积木,构建出各种物体。 面向对象编程更进一步,思路是将逻辑和数据合并起来,构成一个个类,类中包含各种数据和函数,外部只能借助公开函数操纵这些数据。这种以对象表示现实事物的方式与人的思维更加接近,简化了抽象化的难度。 泛型编程则欲将逻辑和数据分离,并提供一种抽象化数据的方式,使得不同的数据能够使用同一种逻辑,极大降低了代码的重复性。于是,同一个函数能够传递不同的参数类型,这叫函数模板;同一个类能够传递不同的数据,这叫类模板。 C++ 是一种多范式语言,以模板支持泛型编程,允许编写泛型的函数和数据,功能不依赖于某种具体的数据类型,使用 SFINAE 和 Concepts 约束抽象化的类型。这种能力使其支持 Parametric Polymorphism,在多态的选择上不必局限于传统的 Subtype Polymorphism,可以使用编译期多态替代运行期多态。 C++ 中,元编程指的是发生于编译期的编程,模板为 C++ 带来泛型编程的同时,也带来了元编程能力。由模板实现的元编程,称为模板元编程。而产生式元编程,指的是发生于编译期的对于编程的编程,模板本就支持在编译期生成代码,因此模板也能够实现产生式元编程。 抽象化、具体化……模板 在模板世界中,函数不再是具体的函数,数据类型也不再是具体的数据类型。模板宛如一个模具,借其能够产生各式各样的物体,这些物体的颜色、材料可能不尽相同,但是形状、大小是一致的。也就是说,一类物体,必须存在共同的部分,才能够抽象出一个模具,模具的作用就是重复利用这些共同之处,减少重复性。 编程是工具,本质是解决问题,解决问题考验的就是抽象化和具体化的能力。抽象化应对的是现实世界中的不变,而具体化应对的是现实世界中的变化,能够清楚地认识到变与不变是什么,问题也就迎刃而解了。那先来分清抽象化和具体化的概念。 抽象的意思是,在许多事物中,去除非本质的属性,抽出本质属性。抽象化就是呈现出具体事物共同本质的过程。将复杂的现实,简化成单纯的模型,这种抽象化方式称为模型化,所谓问题建模,就是对问题进行模型化,也即抽象的过程。具体的概念相对简单,就是指看得见摸得着的事物,每个事物都是独一无二的,是以变化尤盛。具体化就是将抽象事物加载清晰的过程,是一种有助于理解陌生事物的方式。数据类型是具体事物,能够清晰简明地呈现出不同数据类型的共同逻辑,就是用模板类型代替具体类型的精妙之处。 模板本身是一种抽象化的工具,以其编写的是抽象逻辑,表示某类类型的共有本质,是不变的部分。其本身是没有用的,实际需要的还是具体逻辑,因此需要有具体化的过程,将抽象逻辑转变为具体逻辑。在 C++ 模板中,这种将抽象逻辑转变成具体逻辑的过程,称为实例化。实例化将在编译期将抽象类型替换成具体类型,根据模板生成一个个具体逻辑。这种实例化机制,便是我们所需要的代码生成能力。 与宏不同,模板具体化后生成的代码,并无法直接在生成的源码中看到,但是可以通过 CppInsight 这类工具察看。 比如: template <typename T> inline constexpr T Integer = 42; int… Continue Reading 《产生式元编程》第五章 忆昔年模板三两事

宏部分完结 本系列断更良久,去年已更新前三章: 《产生式元编程》 第一章 宏编程计数引原理 《产生式元编程》 第二章 自复用代码生成技 《产生式元编程》 第三章 替换蓝染概念纤悉 宏部分的核心理论和技术,于此三章,悉已更讫。利用这些原理和技巧,可以实现一些简单的代码生成工具,得到初步的产生式元编程能力。 本系列原定十章,宏只是其中的第一个模块,属于上篇。上篇完结后,将产生一个产生式元编程库,再步入中篇。由是,本系列由虚向实,自理论技术诞生项目,复从项目推进续篇,循此迭代。 章章衔进,写来岂是容易?再有诸事相羁,停更半载有余,亦是无奈。近来乘暇推进,初版已卒,遂以续更。 本章乃承上启下之篇,为上篇画句号,为中篇起草稿。 GMP 产生式元编程库 本系列将同步产生一个产生式元编程库,名为 GMP,可在 GitHub 找到 https://github.com/lkimuk/gmp 。该库的侧重点是提供产生式编程工具,可在编译期实现代码生成。 基于前三章的内容,已添加宏模块的基本功能,跨平台,可实现简单的代码生成。其余模块将随着后续章节持续添加。 下面是一些使用例子。 获取可变宏参数数量: #include <gmp/gmp.hpp> int main() { // Output: 0 1 2 3 4 5 6 7 printf("%d %d %d %d %d %d %d %d\n", GMP_SIZE_OF_VAARGS(),… Continue Reading 《产生式元编程》第四章 封装合并框架顿立

前两章主要集中于应用实践,理论概念都是蜻蜓点水,本章将重点放在这些概念原理上,深入讲解一下。 宏二段替换 源文件扫描后,宏被替换为目标内容,替换实际上分为两个阶段。 第一阶段的替换发生在参数替换之时。 当宏函数含有参数时,该参数会被替换。只有两种情况例外,一是 token 前面含有 # 或 ## 预处理标记,二是后面紧跟着 ## 预处理标记。 例如: #define Def(x) x #define Function(x, y) x ## y #define Func(x) #x #define Fun(x) ##x // Not allowed #define Fn(x) x# // Not allowed #define Defn(x, y) # x ## y // Not allowed #define A()… Continue Reading 《产生式元编程》第三章 替换蓝染概念纤悉

衔引 原理毕,复用续。历观编程概念,避及重复,提其不变,于迭代与递归为甚。产生式元编程,乃欲自动产生百千代码,迭代递归,自是重要组件,不可不备。 本章以 FOR_EACH 为例,渐次派生问题,引出技术要点,实现强大组件。 迭代 FOR_EACH 用来迭代数据集合,依次取出数据,后调用函数处理。参数易定,一个回调函数加上可变参数。代码表示: #define FOR_EACH(call, …) // Invoke FOR_EACH(Foo, 1, 2, 3) 实现需逐个取出可变宏参数,调用 Foo() 处理。上章已讲解重载之实现法,此乃条件逻辑在宏中的表示法。无循环时,递归思想便是拆解参数包的唯一方式,将重载实现法与递归思想结合起来,即可达逐个取参数之效。 遂可如此表示: #define _FOR_EACH_1(call, x) call(x) #define _FOR_EACH_2(call, x, …) call(x) _FOR_EACH_1(call, __VA_ARGS__) #define _FOR_EACH_3(call, x, …) call(x) _FOR_EACH_2(call, __VA_ARGS__) #define _FOR_EACH_4(call, x, …) call(x) _FOR_EACH_3(call, __VA_ARGS__) #define _FOR_EACH_5(call, x, …)… Continue Reading 《产生式元编程》第二章 自复用代码生成技

宏编程的基本原理都已暗含于解决前两节问题的过程当中,本节开始,依此继续展开。 经由前面的努力,我们已掌握条件逻辑在宏编程的表示,代码生成往往涉及循环,所以今天来讲如何实现一个 FOR_EACH。 FOR_EACH 用来迭代数据集合,依次取出数据,然后调用函数处理。因此,参数很容易确定,一个回调函数加上可变参数。代码表示: #define FOR_EACH(call, …) 至于展开参数,可以利用重载技术加上递归思想,因为重载技术在前文已经有相关组件,所以实现起来非常简单。 #define _FOR_EACH_1(call, x) call(x) #define _FOR_EACH_2(call, x, …) call(x) _FOR_EACH_1(call, __VA_ARGS__) #define _FOR_EACH_3(call, x, …) call(x) _FOR_EACH_2(call, __VA_ARGS__) #define _FOR_EACH_4(call, x, …) call(x) _FOR_EACH_3(call, __VA_ARGS__) #define _FOR_EACH_5(call, x, …) call(x) _FOR_EACH_4(call, __VA_ARGS__) #define FOR_EACH(call, …) \ OVERLOAD_HELPER(_FOR_EACH_, COUNT_VARARGS(__VA_ARGS__))(call, __VA_ARGS__) 通过层层递归,便可以把大问题拆分成一个个小问题。如果数据集合大小为 5,那么就从… Continue Reading T230925 Generative Metaprogramming with Macro Preprocessor (Part 3)

经过上篇分析实现,第一个需求「计算可变宏参数个数」已由 COUNT_VARARGS 基本实现。 让我们先总结一下用到的思想和发现的技术,再进入下一步。 2.1 通过增加一个间接层,能够解决无法直接解决的问题。 2.2 小步快走,由特殊逐渐扩展到普遍,能够降低问题的解决难度。 2.3 规范过程,确认变与不变,逐步控制变量,能够全面分析问题。 2.4 尝试改变输入的顺序、大小、个数…… 也许能有新发现。 2.5 初步发现规律时,扩大样本验证,能够将特殊推到普遍。 2.6 可变宏参数作为输入和欲输出结果组合起来,其间规律可以表达条件逻辑。 2.7 扩展编译器及语言版本,能够更全面地测试解决方案的普遍性。 那么本篇就来进一步完善解决方案,使它支持 C++20 以下版本。 问题的关键在于 ##__VA_ARGS__ 不具备通用性,此时一种精妙的技术是分情况讨论,即零参和多参分别处理。考虑上节分析过程中的一条失败道路: #define GET_VARARGS_2(a, b) 2 #define GET_VARARGS_1(a) 1 #define GET_VARARGS(…) GET_VARARGS_X(__VA_ARGS__) #define COUNT_VARARGS(…) GET_VARARGS(__VA_ARGS__) 这是函数重载的思路,由于必须确定 GET_VARARGS_X 中的 X,而 X 又和可变宏参数相关,可变宏参数又属于无限集,因而无法确定这个 X,导致此路不通。但是,如果改变前提条件,使 X 的值属于有限集,这条路便可走通,这里便能够利用起来。 只考虑零参和多参的情况,X 的取值范围就可以定在 0… Continue Reading T230920 Generative Metaprogramming with Macro Preprocessor (Part 2)

这期开始将写一些关于宏编程的内容,讲解宏作为代码生成工具在产生式元编程中的运用。 有人可能会有疑问,已有模板作为元编程工具,为何还需要使用宏这种古老的代码生成工具?自然是因为如今模板元编程的代码生成能力仍有不足,在第三阶段 C++ 元编程的核心工具「源码生成」进入标准之前,宏依旧是一种常用的代码生成工具。 文章定位是 TGS,所以每 Part 只会讲一个单独的技术点,利于循序渐进地吸收。 宏编程得从 Variadic Macros 开始谈起,可变模板参数的个数可以通过 sizeof…(args) 获取,宏里面如何操作呢?便是本篇的主题。 首先需要明确,宏只有替换这一个功能,所谓的复杂代码生成功能,都是基于这一核心理念演绎出来的。因此,只要小步慢走,层层递进,复杂能力其实也并不复杂。 我们的需求是获取宏参数包的个数,第一步应该规范过程。 过程的输入是一系列对象,对象类型和个数是变化的;过程的输出是一个值,值应该等于输入对象的个数。如果将输入替换为任何类型的其他对象,只要个数没变,它的结果应该保持不变。 于是通过宏函数表示出过程原型: #define COUNT_VARARGS(…) N 根据宏的唯一功能可知,输出只能是一个值。依此便可否定遍历迭代之类的常规方式,可以考虑多加一层宏,通过由特殊到普遍的思想来不断分析,推出最终结果。 #define GET_VARARGS(a) 1 #define COUNT_VARARGS(…) GET_VARARGS(__VA_ARGS__) 目前这种实现只能支持一个参数的个数识别,我们通过假设,在特殊的基础上逐渐增加更多参数。于是得到: #define GET_VARARGS(a, b) 2 #define GET_VARARGS(a) 1 #define COUNT_VARARGS(…) GET_VARARGS(__VA_ARGS__) 如果该假设成立,通过暴力法已能够将特殊推到普遍,问题也就解决了。但是,宏并不支持重载。有没有可能实现呢?通过再封装一层,消除名称重复。 #define GET_VARARGS_2(a, b) 2 #define GET_VARARGS_1(a) 1 #define GET_VARARGS(…) GET_VARARGS_X(__VA_ARGS__)… Continue Reading T230917 Generative Metaprogramming with Macro Preprocessor (Part 1)