miniblink49 仓库内 Google Test Pump 元编程工具手册:用 `foo.pump` 批量生成 C++ 模板与宏样板代码
2026/9/18 9:16:51 网站建设 项目流程

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的处理管线分为三个阶段(对应文件中的TokenizeParseToASTRunCode等函数):先按词法规则切分$var$range$for$if[[]]等 Token,再解析成 AST(VarNodeRangeNodeForNodeIfNodeRawCodeNodeLiteralDollarNode等节点类),最后在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; };

这个例子一次展示了四个关键行为:

  1. $range i 0..n定义迭代变量i的范围,..两侧都是闭区间端点,且端点n引用前面$var定义的元变量;
  2. $for i [[ ... ]]i从 0 到 3 循环展开块内代码,每次迭代把$i替换为当前值;
  3. 嵌套的$for j 1..i为每个Foo$i生成数量可变的模板参数typename A1, typename A2, ...
  4. $if/$elif/$else根据i的值选择不同的类成员代码。

从 pump.py 的实现可以印证闭区间语义:RunAtomicCodeForNode的执行代码是for i in range(lower, upper + 1):,并且当i不是最后一个值时会在每次迭代之间追加分隔符(sep)。表达式求值则直接交给 Python 的evalEvalExpresult = eval(exp.python_exp)),所以i == 0i <= 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定义。$idcode内有效。
$($)生成一个$字符。
$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

几个要点:

  • expPython 语法的简单表达式,所以可以在$range$if$(...)中直接使用 Python 的算术与比较运算;
  • $var id = [[ code ]]允许把一段展开后的代码(一个元词法块)赋值给元变量,这是实现代码复用与拼接的关键手段;
  • else_branch支持链式的$elif(文法中$elif exp [[ code ]] else_branch递归出现),最终可以以$else收尾,也可以为空。

从源码角度印证:pump.py$var的解析确实分两种情形——右侧若紧跟[[则解析为一个代码块(VarNodeatomic_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批量生成ValueArray1ValueArray50的类模板:

$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)

手册最后给出两条高频实用技巧:

  1. 变量与字母/数字粘连:当元变量后面紧跟字母或数字时,用[[]]插入空字符串来分隔。例如Foo$j[[]]Helperj为 1 时生成Foo1Helper[[]]之间是空代码块,展开结果为空串,恰好起到“分词符”作用。
  2. 避免超长源码行:想在任意位置断行时,插入[[]]后接换行即可。由于紧邻[[]]的换行符会被忽略,生成的代码不会包含这个换行,从而既保持了.pump源文件的可读性,又不会污染输出。

小结与上手路径

Pump 的核心价值一句话概括:把“参数个数维度”上的机械重复交给元代码,让手写的部分只保留真正的逻辑差异。它的语法面很小($var/$range/$for/$if/$elif/$else/[[ ]]/$($)/$$),表达式直接使用 Python 语法,实现是单文件 Python 脚本,因而极易嵌入任何构建流程。

如果你想亲手实践,推荐的上手路径:

  1. 通读本手册与 PumpManual.md(仓库内更新的同主题手册);
  2. 对照 pump.py 的 docstring(其中内嵌了 USAGE 和完整 GRAMMAR);
  3. 打开 gtest-param-util-generated.h.pump 这类真实文件,体会元代码与 C++ 代码如何交织;
  4. 新建一个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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询