C++ / a working model

96 / 103   ·   C++17   ·   约 11 分钟

折叠表达式:空输入、短路与求值顺序

先记住这句话

把多个条件组合成一个验证器时,先定义空集合和失败之后的行为,再选择折叠运算符。C++17 的逻辑与折叠保留短路,但不能让不合法的模板分支免于编译。

本篇内容
  1. 先写组合规则,不先写省略号
  2. 短路发生在运行时,合法性检查发生在编译时
  3. 括号方向不是所有运算符的执行顺序
  4. 运行示例
  5. 动手练习
READING EVIDENCE / 已读部分正文

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 的断言。

继续查证

标准草案链接会随工作草案更新;本文版本标记对应示例最低要求,不表示草案中的所有新规则都适用于旧标准。

回到目录