1. 从一次“丢了初始顺序”的现场说起:sort与stable_sort不是同一回事
1.1 接到的bug:同组数据的相对顺序变得不可解释
先讲个几天前我实际碰到的事。线上服务有一段逻辑,要按 group_id 把一批 Item 分组排序,并且业务方要求:同组的 item 保持原先构造时的相对顺序。原代码长这样:
cpp复制struct Item {
int id; // 全局单调递增,可近似看作初始顺序
int group_id;
std::string name;
};
auto by_group = [](const Item& a, const Item& b) {
return a.group_id < b.group_id;
};
std::ranges::sort(items, by_group);
代码本身能编译,单测也过了。结果上了更大的数据集之后,有同事发现同一 group_id 内部顺序经常“抽风”:明明 id 小一些的对象应该先展示,实际却排在后面,而且每次数据量一大,乱的位置还不一样。
最典型的困惑是:std::ranges::sort 不也是排序吗?我用同一个比较器,为什么以前在 C++14 下用 std::sort 没出事,迁到 C++20 之后顺序变了?
答案其实和 std::ranges 本身没什么关系。std::sort 从一开始就不保证排序稳定性,std::ranges::sort 也一样。以前没出事,只是没有触发足够多的等价元素,或者数据规模恰好小于某个内部阈值,让漏洞没暴露出来。C++20 下标准库实现和调用路径很可能换了,内部排序行为一变,老问题才开始显形。
1.2 比较器里只写一个键,等于告诉算法“其他键我都不在乎”
很多人没有真正意识到一件事:比较器 a.group_id < b.group_id 只比较了 group_id。在排序算法看来,所有 group_id == 1 的 item 之间是不可区分的,它们处在同一个“等价类”。算法没有义务去分辨它们的 id、name 差别,当然也没有义务保持它们原来的先后次序。
用 std::ranges::sort 时更是如此,因为它底层的快排序类算法在反复分割区间、交换元素,凡是比较器认为“等价”的元素,随时可能被交换到任意位置。
问题在于,业务上你说“同组内部要保持初始顺序”,但比较器并没有把这个需求表达出来。对于排序函数来说,这根本不算 bug,因为你没有要求它做这件事。
想让同一组内的顺序确定,通常有两个方向:要么用 std::ranges::stable_sort,让排序函数负责维持等价元素之间的相对位置;要么在比较器里加一个次键作为“平局裁决器”,让本来等价的元素变得不完全等价:
cpp复制auto by_group_preserve_id = [](const Item& a, const Item& b) {
if (a.group_id != b.group_id) {
return a.group_id < b.group_id;
}
return a.id < b.id; // 打破等价性,让顺序可预测
};
std::ranges::sort(items, by_group_preserve_id);
这两种方式都能满足业务需求,但语义并不一样,后面第六节我会仔细对比。这里先记住一个结论:非稳定排序本身不是错,错的是你已经暗中依赖了稳定性,却在代码里没有任何表达。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 严格弱序才是比较器的法律:等价关系如何由comp定义
2.1 std::ranges排序算法对比较器的真实要求
先说接口。std::ranges::sort 的常见签名大致是:
cpp复制template<std::ranges::random_access_range R,
class Comp = std::ranges::less,
class Proj = std::identity>
requires std::sortable<std::ranges::iterator_t<R>, Comp, Proj>
std::ranges::borrowed_iterator_t<R>
sort(R&& r, Comp comp = {}, Proj proj = {});
这里 Comp 被 std::sortable 约束住,而 std::sortable 又要求这样的关系:
comp是一个二元谓词,能接受两个元素(投影后)进行比较;comp必须满足严格弱序(strict weak ordering)。
严格弱序听起来抽象,翻译成人话就是“小于”这种关系必须具备四条性质。我建议把它写下来,排查比较器问题时对照着看很有效:
| 性质 | 形式化描述 | 直觉 |
|---|---|---|
| 非自反 | comp(a, a) 必须为 false |
没有任何元素小于自己 |
| 非对称 | 如果 comp(a, b) 为 true,则 comp(b, a) 必须为 false |
两者不能互相“小于”对方 |
| 传递 | 如果 comp(a, b) 且 comp(b, c),则必须有 comp(a, c) |
小于关系可以一路传下去 |
| 等价性传递 | 定义 equiv(a,b) 为 `! |
