miniblink49 仓库内 Google Test Pump 元编程工具手册:用foo.pump批量生成 C++ 模板与宏样板代码
【免费下载链接】miniblink49a lighter, faster browser kernel of blink to integrate HTML UI in your app. 一个小巧、轻量的浏览器内核,用来取代wke和libcef项目地址: https://gitcode.com/GitHub_Trending/mi/miniblink49
Pump(Pump is Useful for Meta Programming / Pretty Useful for Meta Programming / Practical Utility for Meta Programming)是 Google Test 附带的 C++ 元编程工具:你只需写一个.pump源文件,在其中混入少量以$开头的元代码,就能自动展开成成百上千行结构重复的 C++ 模板、函数和宏。本手册以 V1_5_PumpManual.md 为骨架,结合本仓库 pump.py 与真实.pump用例(如 gtest-param-util-generated.h.pump)展开讲解。读完本文,你将掌握 Pump 的完整语法($var、$range、$for、$if、[[ ]]、$$注释等)、运行方式、语法规则与实战技巧,并能自己编写.pump文件来生成参数化、类型化的 C++ 代码。
为什么需要 Pump:模板库的“参数数量爆炸”问题
模板库和宏库经常需要定义大量仅在参数个数上不同(或几乎不同)的类、函数或宏。例如 Google Test 的Values()参数化测试接口,需要同时支持 1 个、2 个……直到 50 个参数,每个参数个数对应一个ValueArrayN类;Google Mock 的gmock-generated-*系列头文件要为 0 到 10 个参数的函数生成对应的 mock 辅助模板。这是大量重复、机械且极易出错的工作。
变参模板(variadic templates)和变参宏(variadic macros)可以缓解这个问题,但即便在编写本手册的年代,它们尚未进入 C++ 标准、也未被编译器广泛支持,尤其是在追求可移植性的代码中并不总是好选择,而且能力仍然有限。
于是库作者通常选择编写脚本来生成实现。但手写生成脚本本身也很繁琐:脚本往往难以反映生成代码的结构、可读性和可维护性差,生成代码里一个很小的改动,可能需要在脚本中做一系列不直观、不小的调整——这在实验阶段尤其痛苦。Pump 正是为解决这一痛点而设计。
Pump 的解决方案与设计亮点
Pump 的思路很简单:程序员写一个foo.pump文件,其中既包含 C++ 代码,又包含操纵这些 C++ 代码的元代码。元代码支持:范围迭代、嵌套迭代、局部元变量定义、简单算术、条件表达式。你可以把它看作一个小型领域专用语言(DSL),元语言被刻意设计为“非侵入式”(例如不会干扰 Emacs 的 C++ 模式)且足够简洁,使 Pump 源码直观易维护。
Pump 的几个核心亮点:
- 单文件 Python 脚本,极度可移植:完整实现就在一个 pump.py 中,无需构建、无需安装、跨平台直接运行。
- 智能排版:Pump 会尽量遵循 Google 代码风格指南,在合适的位置断开超长行(生成代码很容易超长),使其不超过 80 列,并正确缩进续行。
- 比 XML 更人性化:格式可读性好、更简洁。
- 与 Emacs C++ 模式兼容良好。
安装与运行:零依赖,一条命令
Pump 无需安装。运行方式在 pump.py 的 docstring 中有明确说明:
USAGE: pump.py SOURCE_FILE EXAMPLES: pump.py foo.cc.pump Converts foo.cc.pump to foo.cc.也就是说,foo.pump会被转换成同名的foo(去掉.pump后缀)文件。在仓库中,你可以这样验证真实用例的生成结果,例如将 gtest-param-util-generated.h.pump 重新生成为同名头文件。
从源码结构看,pump.py的处理管线分为三个阶段(对应文件中的Tokenize、ParseToAST、RunCode等函数):先按词法规则切分$var、$range、$for、$if、[[、]]等 Token,再解析成 AST(VarNode、RangeNode、ForNode、IfNode、RawCodeNode、LiteralDollarNode等节点类),最后在Env环境中求值并拼接输出。它识别元关键字的词法表(TOKEN_TABLE)依次为:$var、$elif、$else、$for、$if、$range、$id、$($)、单独的$、[[与]]。
语法速览:三个 Token 概念与元注释
在进入示例前,先记住三个基本约定(手册原文如此):
- 元关键字以
$开头; [[和]]是元括号(meta brackets);$$开启一条元注释,注释持续到行尾。
一个最小的“Hello”级示例展示了注释与变量:
$var n = 3 $$ Defines a meta variable n.$$后面的内容全部是元注释,不会出现在生成代码中。
第一个完整示例:生成 0 到 3 元的模板类
手册给出的第一个完整示例同时用到了$var、$range、$for、$if/$elif/$else和嵌套迭代:
$var n = 3 $$ Defines a meta variable n. $range i 0..n $$ Declares the range of meta iterator i (inclusive). $for i [[ $$ Meta loop. // Foo$i does blah for $i-ary predicates. $range j 1..i template <size_t N $for j [[, typename A$j]]> class Foo$i { $if i == 0 [[ blah a; ]] $elif i <= 2 [[ blah b; ]] $else [[ blah c; ]] }; ]]Pump 编译器会将其翻译成如下 C++ 代码(注意i的取值范围是闭区间0..n,即 0、1、2、3):
// Foo0 does blah for 0-ary predicates. template <size_t N> class Foo0 { blah a; }; // Foo1 does blah for 1-ary predicates. template <size_t N, typename A1> class Foo1 { blah b; }; // Foo2 does blah for 2-ary predicates. template <size_t N, typename A1, typename A2> class Foo2 { blah b; }; // Foo3 does blah for 3-ary predicates. template <size_t N, typename A1, typename A2, typename A3> class Foo3 { blah c; };这个例子一次展示了四个关键行为:
$range i 0..n定义迭代变量i的范围,..两侧都是闭区间端点,且端点n引用前面$var定义的元变量;$for i [[ ... ]]对i从 0 到 3 循环展开块内代码,每次迭代把$i替换为当前值;- 嵌套的
$for j 1..i为每个Foo$i生成数量可变的模板参数typename A1, typename A2, ...; $if/$elif/$else根据i的值选择不同的类成员代码。
从 pump.py 的实现可以印证闭区间语义:RunAtomicCode中ForNode的执行代码是for i in range(lower, upper + 1):,并且当i不是最后一个值时会在每次迭代之间追加分隔符(sep)。表达式求值则直接交给 Python 的eval(EvalExp中result = eval(exp.python_exp)),所以i == 0、i <= 2这类判断用的就是 Python 语法。
第二个示例:迭代分隔符的用法
$for指令的完整形态是$for id sep [[code]],其中id与[[之间的文本就是每次迭代之间的分隔符。手册示例:
$range i 1..n Func($for i + [[a$i]]); $$ The text between i and [[ is the separator between iterations.即迭代变量i与[[之间的+是分隔符。根据n的值会生成:
Func(); // If n is 0. Func(a1); // If n is 1. Func(a1 + a2); // If n is 2. Func(a1 + a2 + a3); // If n is 3. // And so on...注意范围1..n的取值从 1 开始,因此n为 0 时循环体一次都不执行,只输出Func();。这正是 Google Test/Google Mock 头文件里template <$for j, [[typename T$j]]>这类写法的来源:j与[[之间的,充当模板参数列表的分隔符。
支持的元编程构造(完整参考表)
手册将所有构造汇总如下(表格已按 Markdown 规范整理):
| 构造 | 含义 |
|---|---|
$var id = exp | 定义命名常量值。$id在当前元词法块(meta lexical block)结束前有效。 |
$range id exp..exp | 设置迭代变量的范围,之后可被多个循环复用。 |
$for id sep [[code]] | 迭代。id的范围必须事先用$range定义。$id在code内有效。 |
$($) | 生成一个$字符。 |
$id | 命名常量或迭代变量的值。 |
$(exp) | 表达式的值。 |
$if exp [[ code ]] else_branch | 条件分支。 |
[[ code ]] | 元词法块。 |
cpp_code | 原始 C++ 代码(原样透传)。 |
$$ comment | 元注释。 |
重要的换行规则:为了给用户排版自由,Pump 会忽略紧跟$for foo之后、或紧邻[[/]]的换行符。如果没有这条规则,你常常被迫写出非常长的行才能得到想要的输出。因此,有时你需要在这些位置额外插入一个换行,才能在输出中得到换行。
正式文法(Grammar)
手册给出 Pump 的完整文法定义:
code ::= atomic_code* atomic_code ::= $var id = exp | $var id = [[ code ]] | $range id exp..exp | $for id sep [[ code ]] | $($) | $id | $(exp) | $if exp [[ code ]] else_branch | [[ code ]] | cpp_code sep ::= cpp_code | empty_string else_branch ::= $else [[ code ]] | $elif exp [[ code ]] else_branch | empty_string exp ::= simple_expression_in_Python_syntax几个要点:
exp是Python 语法的简单表达式,所以可以在$range、$if、$(...)中直接使用 Python 的算术与比较运算;$var id = [[ code ]]允许把一段展开后的代码(一个元词法块)赋值给元变量,这是实现代码复用与拼接的关键手段;else_branch支持链式的$elif(文法中$elif exp [[ code ]] else_branch递归出现),最终可以以$else收尾,也可以为空。
从源码角度印证:pump.py中$var的解析确实分两种情形——右侧若紧跟[[则解析为一个代码块(VarNode的atomic_code分支),否则解析为一个表达式(ParseExpNode);ParseElseNode函数则专门处理$else与$elif的递归解析。
真实用例:Google Test / Google Mock 中的.pump文件
本仓库的v8_7_5/testing目录下就保存着 Pump 在 Google Test 和 Google Mock 中的真实应用,.pump文件会生成同名(去掉后缀)的头文件:
- gtest-param-util-generated.h.pump(生成
gtest-param-util-generated.h) - gtest-type-util.h.pump
- gtest-tuple.h.pump
- gtest-param-test.h.pump
- gmock-generated-actions.h.pump
- gmock-generated-function-mockers.h.pump
- gmock-generated-matchers.h.pump
- gmock-generated-nice-strict.h.pump
- gmock-generated-internal-utils.h.pump
- gmock-generated-actions.h.pump
以 gtest-param-util-generated.h.pump 为例,文件开头就定义了生成规模参数:
$$ -*- mode: c++; -*- $var n = 50 $$ Maximum length of Values arguments we want to support. $var maxtuple = 10 $$ Maximum number of Combine arguments we want to support.这两个元变量决定了生成代码的上限:Values()最多支持 50 个参数,Combine()最多支持 10 个参数(后者同时受 tuple 实现的最大元数限制)。随后用$range i 1..n、$for i加上内层$range j 1..i、$for j批量生成ValueArray1到ValueArray50的类模板:
$range i 1..n $for i [[ $range j 1..i template <$for j, [[typename T$j]]> class ValueArray$i { public: $if i==1 [[explicit ]]ValueArray$i($for j, [[T$j v$j]]) : $for j, [[v$(j)_(v$j)]] {} ...这里能看到前面所有构造的综合运用:外层$for i枚举参数个数,内层$for j生成参数列表,$if i==1控制单参数构造函数是否加explicit,$for j, [[...]]中的,作为分隔符拼接成员初始化列表。同时注意$($)转义的需求:文件末尾的版权声明、宏名GTEST_INCLUDE_...中的真实$并不存在,但若你需要在生成的 C++ 代码中输出美元符号,就必须使用$($)。
对应的生成结果头文件(如 gmock-generated-function-mockers.h)第二行会留下生成记录// pump.py gmock-generated-function-mockers.h.pump,正文开头还有// This file is generated by a SCRIPT. DO NOT EDIT BY HAND!的警告——这正是 Pump 生成流水线的指纹:改.pump、跑pump.py,而不是手改生成物。
README.md 也明确建议:修改这些由脚本生成的头文件时,应修改对应的.pump文件并用pump.py重新生成;Makefile.am 中同样把scripts/pump.py列为发行内容的一部分,说明它作为构建链一环的正式地位。
实用技巧(Tips)
手册最后给出两条高频实用技巧:
- 变量与字母/数字粘连:当元变量后面紧跟字母或数字时,用
[[]]插入空字符串来分隔。例如Foo$j[[]]Helper在j为 1 时生成Foo1Helper。[[与]]之间是空代码块,展开结果为空串,恰好起到“分词符”作用。 - 避免超长源码行:想在任意位置断行时,插入
[[]]后接换行即可。由于紧邻[[或]]的换行符会被忽略,生成的代码不会包含这个换行,从而既保持了.pump源文件的可读性,又不会污染输出。
小结与上手路径
Pump 的核心价值一句话概括:把“参数个数维度”上的机械重复交给元代码,让手写的部分只保留真正的逻辑差异。它的语法面很小($var/$range/$for/$if/$elif/$else/[[ ]]/$($)/$$),表达式直接使用 Python 语法,实现是单文件 Python 脚本,因而极易嵌入任何构建流程。
如果你想亲手实践,推荐的上手路径:
- 通读本手册与 PumpManual.md(仓库内更新的同主题手册);
- 对照 pump.py 的 docstring(其中内嵌了 USAGE 和完整 GRAMMAR);
- 打开 gtest-param-util-generated.h.pump 这类真实文件,体会元代码与 C++ 代码如何交织;
- 新建一个
foo.pump,尝试用$range+ 嵌套$for生成不同元数的模板类,再用python pump.py foo.pump检查输出是否符合预期。
当你需要为 C++ 库生成“参数个数变化”的模板、函数或宏,而又不想维护脆弱的手写脚本时,Pump 是一个经过 Google Test / Google Mock 实战检验的轻量选择。
【免费下载链接】miniblink49a lighter, faster browser kernel of blink to integrate HTML UI in your app. 一个小巧、轻量的浏览器内核,用来取代wke和libcef项目地址: https://gitcode.com/GitHub_Trending/mi/miniblink49
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考