C++ / a working model

103 / 163   ·   C++17   ·   约 15 分钟

算法与配接器:先证明区间,再组合操作

先记住这句话

读懂 STL 源码不是记住内部类名,而是分清输入区间、输出责任与调用契约。用一段完整的数据处理程序连接插入配接器、严格弱序、反向边界和成员调用,并说明旧式函数配接器如何安全改写。

本篇内容
  1. 输出配接器改变的是操作的含义
  2. 排序与反向遍历都有精确边界
  3. 组合调用不意味着自动写回
  4. 运行示例
  5. 动手练习
READING EVIDENCE / 已读完整正文

STL 源码剖析 / The Annotated STL Sources

实际逐页读取第 1–8 章、附录 A–C 与索引的文字(PDF 34–527,印刷页 1–494),并直接查看全部已识别的重要图示及代码疑点。第 1–6 章由分工阅读报告记录,第 7–8 章与附录由主阅读者完成;历史实现错误另作区分,不声称执行了书中全部代码。附录中的其他书籍推荐不等于那些书也已经读完。

查看版本、实际阅读范围与原文入口 →

输出配接器改变的是操作的含义

读完插入迭代器源码后,应先问赋值究竟写到哪里。普通 vector 迭代器表示已有元素的位置,向它赋值不会增加 size;back_inserter 保存对容器的访问关系,把赋值解释为追加一个元素。算法负责何时输出,容器负责保存新对象,两个职责因此可以组合。

本例从只读原始记录生成拥有数据的新记录。输出容器最初为空,所以使用 back_inserter,而不是把 begin 当作现成空间。即使事先 reserve 也不改变这个结论。转换会复制姓名,是因为结果需要独立拥有它;这里不能为了少复制而返回指向短命工作对象的引用。

排序与反向遍历都有精确边界

比较器先按分数降序,再按姓名升序,让并列结果可复现。两个字段都相等时,比较器必须返回 false;使用大于等于会破坏严格弱序。标准约束的是元素之间的关系,不要求任意相邻元素比较都返回 true,也不保证未指定的并列顺序。

反向迭代器保存正向边界,取值时访问其前一个元素。因此 rbegin 的 base 等于 end,但 rbegin 与 end 并不表示同一个可解引用对象。例子断言最后元素和边界关系,却绝不读取 end 或 rend;书中出现的危险运行结果不能移植成正常示例。

组合调用不意味着自动写回

for_each 调用可调用对象,但不会把函数返回值自动写回元素。需要生成输出时选择 transform;需要显式副作用时,可让回调接收可写引用。本例先转换记录,再用 mem_fn 调用每个记录的只读成员,收集结果,数据修改与观察分开。

mem_fn 是对旧式 mem_fun 家族的现代替代之一,能处理不同对象访问形式;lambda 通常更直观。C++17 的 invoke 统一成员指针调用规则,C++20 ranges 算法也采用相应调用语义。不过经典 for_each 仍不能直接把一个裸成员函数指针当作普通函数调用;适配表达式时仍要检查参数个数、const 和对象生存期。

常见误区

  • reserve 不创建元素;空 vector 的 begin 不能作为非空 transform 的直接输出区间。
  • 反向迭代器的 base 是正向边界,不是同一个元素;end/rend 不可解引用。
  • 排序比较器不能使用 >= 代替 >;等价元素必须满足两个方向都为 false。

运行一个例子

最低标准 C++17 · 完整程序 · 下载 .cpp

#include <algorithm>
#include <cassert>
#include <functional>
#include <iterator>
#include <string>
#include <vector>

struct Record {
    std::string name;
    int score;
    int points() const { return score; }
};

int main() {
    const std::vector<Record> input{{"Bea", 4}, {"Ari", 4}, {"Cy", 2}};
    std::vector<Record> ranked;
    std::transform(input.begin(), input.end(), std::back_inserter(ranked),
        [](const Record& r) { return Record{r.name, r.score + 1}; });
    const auto before = [](const Record& a, const Record& b) {
        if (a.score != b.score) return a.score > b.score;
        return a.name < b.name;
    };
    std::sort(ranked.begin(), ranked.end(), before);
    assert(ranked[0].name == "Ari" && ranked[1].name == "Bea");
    assert(!before(ranked[0], ranked[0]));
    assert(input[0].score == 4 && ranked[0].score == 5);

    auto last = ranked.rbegin();
    assert(last.base() == ranked.end());
    assert(last->name == "Cy");

    std::vector<int> scores;
    const auto points = std::mem_fn(&Record::points);
    std::for_each(ranked.begin(), ranked.end(),
        [&](const Record& r) { scores.push_back(points(r)); });
    assert((scores == std::vector<int>{5, 5, 3}));
    const auto found = std::find_if(ranked.begin(), ranked.end(),
        [](const Record& r) { return r.name == "Bea"; });
    assert(found != ranked.end());
    assert(std::invoke(&Record::points, *found) == 5);
}

在本地编译

g++ -std=c++17 -Wall -Wextra -Wpedantic -pthread books-stl-source-analysis.cpp -o example && ./example

预期结果

预期:正常退出、无输出;所有 assert 通过。

CHECK YOUR UNDERSTANDING

合上答案,试着解释。

把 scores 改为按 ranked 的反向顺序生成,不引入任何新容器。写出完整替换语句并给出断言;是否需要减小 rend?

查看参考答案

替换生成部分为:scores.clear(); std::transform(ranked.rbegin(), ranked.rend(), std::back_inserter(scores), std::mem_fn(&Record::points)); 然后断言 assert((scores == std::vector<int>{3, 5, 5}));。不需要调整 rend;反向半开区间已经覆盖全部三个元素。rend 只是终止边界,算法不会解引用它。

继续查证

标准草案与官方章节会更新;版本标记只说明示例最低要求。

回到目录