C++ / a working model

94 / 103   ·   C++11   ·   约 13 分钟

排序轻量索引:保留原数据顺序,也写清失效规则

先记住这句话

不改变原始记录也能提供排序视图:构造位置索引,只比较源记录中的键。用平局规则得到确定结果,同时说明索引并不管理源对象的生命周期,也不会自动跟随数据修改而重新排序。

本篇内容
  1. 视图顺序不必等于存储顺序
  2. 比较规则包含平局时的决定
  3. 节省搬运,不等于免除维护
  4. 运行示例
  5. 动手练习
READING EVIDENCE / 已读部分正文

Exceptional C++ Style: 40 New Engineering Puzzles, Programming Problems, and Solutions

完整读完出版社40页样章,即Items34–36(印刷页246–285):Index Tables、Generic Callbacks、Construction Unions。已核对40条目录。Items1–33及37–40未读;公开样章不是全书,GotW63–86原始文章也未冒充这些书本条目。

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

视图顺序不必等于存储顺序

Item 34 分析索引表而不是重写一个排序算法。这里把同一思路用于有名称与耗时的任务记录:原数组顺序代表提交次序,另一个索引序列代表从短到长的展示次序。排序只移动 size_t 值,不移动名称和完整记录,也不复制第二份任务列表。

本例选择位置索引而不是保存迭代器,接口因此明确限定为可随机访问的固定数组。没有为了可能出现的任意容器发明适配层;实际需求只要这一个稳定的数据集,简单表示就足够。扩展能力应来自真实使用场景,不是模板参数越多越好。

比较规则包含平局时的决定

比较器通过下标读取任务耗时。耗时不同时按小值优先,相同时按原始位置优先,形成确定的展示顺序。因此断言可以验证确切的索引序列,而不用依赖 std::sort 对等价元素的未承诺排列。若业务只要求保留输入中的同键顺序,也可以选 stable_sort。

比较器捕获源数组引用,这个引用只在同步排序调用期间使用,源数组始终存活且未修改。返回索引并不会复制记录,也不会获得数据所有权;它只是让调用方能够用第二种顺序解释同一批对象。

节省搬运,不等于免除维护

索引必须与源数据一起解释。对于 vector,单纯重新分配不会改变位置编号,但中间插入、擦除或整体重排会改变编号对应的对象;保存的迭代器还会额外受重新分配影响。即使记录位置不变,只更新一个耗时字段,也可能使旧展示顺序过期。

因此使用索引视图前应制定规则:源数据在视图期间冻结,或者修改后重建索引。当前完整程序选前者,用 const 固定数组将规则直接写进类型。书中关于可读性与重用标准库的建议在此体现为减少手写管理逻辑,而不是逐行移植二十年前的辅助类。

容易答错的地方

  • 索引在范围内只证明访问没有越界,不证明它仍指向业务上原来那条记录。
  • 比较器读取的键在排序过程中必须保持一致;不能在比较函数里修改耗时或依赖比较调用次数。

运行一个例子

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

#include <algorithm>
#include <array>
#include <cassert>
#include <cstddef>
#include <numeric>
#include <string>

struct Job {
    std::string name;
    int duration;
};

int main() {
    const std::array<Job, 4> jobs{{
        {"parse", 7}, {"cache", 3}, {"index", 7}, {"send", 1}
    }};
    std::array<std::size_t, 4> order{};
    std::iota(order.begin(), order.end(), std::size_t{0});
    std::sort(order.begin(), order.end(), [&jobs](std::size_t a, std::size_t b) {
        if (jobs[a].duration != jobs[b].duration) {
            return jobs[a].duration < jobs[b].duration;
        }
        return a < b;
    });
    assert((order == std::array<std::size_t, 4>{{3, 1, 0, 2}}));
    assert(jobs[0].name == "parse");
    assert(jobs[order[0]].name == "send");
    assert(jobs[order[2]].name == "parse");
}

在本地编译

g++ -std=c++11 -Wall -Wextra -Wpedantic -pthread books-exceptional-cpp-style.cpp -o example && ./example

预期结果

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

CHECK YOUR UNDERSTANDING

合上答案,试着解释。

改成耗时从大到小,同耗时仍保持原始位置优先,应改哪一处?

查看参考答案

只把不同耗时分支改为 jobs[a].duration > jobs[b].duration;平局仍返回 a < b。结果索引为0、2、1、3,不能整体取反比较结果,否则相同下标会比较为真,破坏严格弱序。

继续查证

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

回到目录