96 / 103 · C++17 · 约 11 分钟
折叠表达式:空输入、短路与求值顺序
把多个条件组合成一个验证器时,先定义空集合和失败之后的行为,再选择折叠运算符。C++17 的逻辑与折叠保留短路,但不能让不合法的模板分支免于编译。
C++ Templates: The Complete Guide
已顺序读完第二版第 1–28 章及附录 A–E 的可读正文和文本代码,另交叉阅读官方第 4、23 章。已检查全部 13 幅实质插图对应的 12 个 PDF 页:图 18.1–18.5、D.1 可见且已读;图 13.1、21.1–21.4、27.1、B.1 在取得的 PDF 中仅剩标题或链接、图形缺失(共 7 幅),因此仍列部分覆盖,不把标题当作图像阅读。图 18.4 的图形位于标题前一页,已核对。实际版本为 2017 年出版、版权 2018 的第二版,不冒充第一版阅读。目录、书目、索引不计正文。书中 Concepts 是 C++20 定稿前设计;旧式 -> bool 语法不能当作最终 C++20。
查看版本、实际阅读范围与原文入口 →先写组合规则,不先写省略号
假设每个检查器接受一个整数并返回真假,组合器只有在所有检查都通过时才成功。这里自然的单位元是真:没有检查器意味着没有条件被违反。因此 all_of_checks(7) 应返回 true,而不是要求额外写一个空参数重载。
示例的 (... && static_cast<bool>(checks(value))) 是一元左折叠。&& 的空包规则提供 true,显式转换又确保使用内建逻辑与,避免某个自定义返回类型通过重载运算符改变短路语义。模板减少的是重复结构,不是对行为边界的说明。
短路发生在运行时,合法性检查发生在编译时
内建 && 先求左操作数;一旦为假,后面的检查器就不再调用。第二组断言让第一个检查必定失败,并记录第二个是否执行,证明没有把“全部条件”误写成总会求值的位运算 &。检查器对象在入口前已经构造完成,短路只约束函数体中的调用,不能撤销调用者构造实参的副作用。
即使第一个检查总返回 false,每一个 checks(value) 仍须是合法表达式。某个类型不能以整数调用,就会导致实例化失败;这不是 if constexpr 的编译期分支丢弃。参数包中各元素可以有不同类型,但必须共同满足组合器真正使用的接口。
括号方向不是所有运算符的执行顺序
书中通过折叠表达式减少递归,本课进一步把它当成一个小型代数契约。对于 &&,左右折叠在真假结果上通常相同;换成减法,(10-3)-2 与 10-(3-2) 就不同。不要从“左折叠”推导任意运算符都按从左到右执行副作用,顺序仍由所选运算符的规则决定。
检查器按 const 引用借用,函数不保存它们,临时 lambda 在完整调用期间有效。这里不使用完美转发,因为多次消费有状态右值并不是接口需求。参数包适合编译期已知的异构检查器;若检查器数量来自配置文件,应使用运行时容器,不必把动态问题硬塞进模板。
容易答错的地方
- 运行时 && 短路不免除每个展开表达式的编译期合法性要求。
- 折叠的括号结合方向不等于任意运算符都保证相同的求值顺序。
运行一个例子
最低标准 C++17 · 完整程序 · 下载 .cpp
#include <cassert>
template<class... Checks>
bool all_of_checks(const int& value, const Checks&... checks) {
return (... && static_cast<bool>(checks(value)));
}
int main() {
assert(all_of_checks(7));
const auto positive = [](int n) { return n > 0; };
const auto even = [](int n) { return n % 2 == 0; };
assert(all_of_checks(8, positive, even));
assert(!all_of_checks(3, positive, even));
int calls = 0;
const auto reject = [&calls](int) { ++calls; return false; };
const auto expensive = [&calls](int) { calls += 100; return true; };
const bool accepted = all_of_checks(8, reject, expensive);
assert(!accepted);
assert(calls == 1);
}
在本地编译
g++ -std=c++17 -Wall -Wextra -Wpedantic -pthread books-cpp-templates.cpp -o example && ./example预期结果
预期:正常退出、无输出;所有 assert 通过。
CHECK YOUR UNDERSTANDING
合上答案,试着解释。
将组合器改为“至少一个通过”,空参数包应该返回什么?
查看参考答案
改成 (... || static_cast<bool>(checks(value)))。内建 || 的空包结果为 false,因为没有任何检查能够提供成功证据;第一个 true 会阻止后续调用。将副作用断言改成第一个检查返回 true、后续检查不执行,并保留无检查器时结果为 false 的断言。
继续查证
标准草案链接会随工作草案更新;本文版本标记对应示例最低要求,不表示草案中的所有新规则都适用于旧标准。