103 / 163 · C++17 · 约 15 分钟
算法与配接器:先证明区间,再组合操作
读懂 STL 源码不是记住内部类名,而是分清输入区间、输出责任与调用契约。用一段完整的数据处理程序连接插入配接器、严格弱序、反向边界和成员调用,并说明旧式函数配接器如何安全改写。
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 只是终止边界,算法不会解引用它。
继续查证
- C++ draft: back_insert_iterator
- C++ draft: reverse_iterator
- C++ draft: sorting and strict weak ordering
- C++ draft: for_each
标准草案与官方章节会更新;版本标记只说明示例最低要求。