第二十章:std::views::zip与enumerate:多范围操作、索引遍历

说实话,C++20 刚出 ranges 库那会儿,我第一反应是:又来了个花架子?

直到有一次我在项目里要同时遍历三个容器,还得带上索引。以前的做法?写个 for 循环,下标 i 从 0 到 size,然后挨个取 vec1[i]、vec2[i]、vec3[i]。代码丑不说,还得时刻提防容器长度不一致。后来我试着用 std::views::zip 重写了一遍——嗯,真香。

这一章咱们就聊聊 std::views::zipstd::views::enumerate。说白了,它们就是帮你把「多范围并行遍历」和「带索引遍历」这两件事,写得又短又安全。

20.1 为什么需要 zip?

你想想看,实际项目里经常有这样的场景:

  • 学生姓名列表和成绩列表,按位置一一对应
  • 传感器 ID 列表和读数列表,需要配对处理
  • 两个时间序列数据,要同步遍历

传统写法长这样:

std::vector<std::string> names = {"Alice", "Bob", "Charlie"};
std::vector<int> scores = {92, 85, 78};

for (size_t i = 0; i < names.size() && i < scores.size(); ++i) {
    std::cout << names[i] << ": " << scores[i] << "\n";
}

这段代码有什么问题?

  • 你得手动管理下标 i
  • 你得自己检查边界,否则越界
  • 可读性一般,别人得看一会儿才明白你在「配对遍历」

zip 改写:

for (auto [name, score] : std::views::zip(names, scores)) {
    std::cout << name << ": " << score << "\n";
}

干净、直接、不会越界。它自动取所有范围的最小长度作为迭代次数。这就是我喜欢的风格——代码自己说明意图。

核心要点zip 把多个范围「拉链」到一起,每次迭代返回一个 tuple(或 pair),里面是每个范围当前位置的元素。

20.2 zip 的进阶用法

不只是两个容器,zip 可以接受任意数量的范围。我在项目中最多一次 zip 了四个 vector,代码依然清晰。

std::vector<int> ids = {1, 2, 3};
std::vector<std::string> names = {"Alice", "Bob", "Charlie"};
std::vector<double> salaries = {5000.0, 6000.0, 5500.0};

for (auto [id, name, salary] : std::views::zip(ids, names, salaries)) {
    std::cout << id << " " << name << " " << salary << "\n";
}

注意一点:zip 返回的是视图(view),不是容器。它不会拷贝数据,只是提供一种「组合遍历」的视角。性能开销几乎为零。

我曾经踩过的坑zip 的迭代器是 input_iteratorforward_iterator,取决于底层范围。你不能对它做 std::sort 之类的操作。如果需要排序,得先把 zip 结果拷贝到 vector 里。

20.3 enumerate:给遍历加上索引

另一个常见需求:遍历容器时,我还想知道当前是第几个元素。

传统写法:

for (size_t i = 0; i < vec.size(); ++i) {
    std::cout << i << ": " << vec[i] << "\n";
}

或者用迭代器:

for (auto it = vec.begin(); it != vec.end(); ++it) {
    auto idx = std::distance(vec.begin(), it);
    std::cout << idx << ": " << *it << "\n";
}

这两种写法都不够优雅。前者容易下标越界,后者性能差(std::distance 对 forward_iterator 是 O(n))。

std::views::enumerate 就是专门解决这个问题的:

for (auto [index, value] : std::views::enumerate(vec)) {
    std::cout << index << ": " << value << "\n";
}

index 从 0 开始,类型是 std::ptrdiff_t(也就是有符号整数)。

个人习惯:我经常把 enumeratezip 组合使用。比如同时遍历两个容器,还要知道当前索引:

for (auto [idx, a, b] : std::views::enumerate(std::views::zip(vec1, vec2))) {
    auto [v1, v2] = a;  // 解包内层 pair
    std::cout << idx << ": " << v1 << ", " << v2 << "\n";
}

不过说实话,这样嵌套可读性会下降。我一般更推荐先 zip 再 enumerate,或者反过来,看具体需求。

20.4 实战:合并多个数据源

我记得有一次做日志分析工具,需要把时间戳、日志级别、消息内容三个数组对齐输出。每个数组长度可能不一样,但前 N 条是配对的。

zipenumerate 写出来是这样的:

std::vector<int64_t> timestamps = {1700000001, 1700000002, 1700000003};
std::vector<std::string> levels = {"INFO", "WARN", "ERROR"};
std::vector<std::string> messages = {"start", "disk full", "crash"};

for (auto [idx, t, lvl, msg] : std::views::enumerate(
         std::views::zip(timestamps, levels, messages))) {
    std::cout << "[" << idx << "] "
              << "time=" << t << " "
              << "level=" << lvl << " "
              << "msg=" << msg << "\n";
}

输出:

[0] time=1700000001 level=INFO msg=start
[1] time=1700000002 level=WARN msg=disk full
[2] time=1700000003 level=ERROR msg=crash

代码量少,逻辑清晰。如果以后要加字段,只需要往 zip 里再加一个 vector 就行,循环体几乎不用改。

20.5 性能与注意事项

很多人担心 ranges 的性能。我实测过,zipenumerate 在开启优化(-O2)后,生成的汇编和手写循环几乎一样。编译器很聪明,能把视图的开销优化掉。

但有几个点要注意:

注意事项 说明
长度不一致 zip 取最短范围的长度,不会报错。如果你需要严格等长,建议先断言
修改元素 如果范围是 const,你不能通过 zip 修改元素。需要非 const 引用
临时对象 不要 zip 临时范围(比如函数返回的 vector),视图不会延长生命周期
迭代器失效 如果底层容器被修改(比如 push_back),zip 视图的迭代器会失效

我曾经犯过的错:有一次我写了 for (auto [i, v] : enumerate(get_data())),结果 get_data() 返回的是临时 vector,循环还没结束 vector 就析构了。嗯,那是一个难忘的调试下午。记住:视图不拥有数据,它只是「看」数据。

20.6 知识体系总览

下面这张图帮你理清 zipenumerate 在整个 ranges 视图中的位置:

C++20 Ranges 视图核心分类 std::views 命名空间 范围适配器 (Range Adapters) 范围工厂 (Range Factories) filter / transform take / drop zip ★ join / split iota empty / single enumerate ★ istream ★ 本章重点:zip 用于多范围并行遍历,enumerate 用于带索引遍历

从图上可以看得很清楚:zip 属于范围适配器,它把多个范围「压缩」成一个;enumerate 属于范围工厂,它生成一个带索引的新视图。两者经常配合使用,但本质上是不同的类别。

20.7 小结

std::views::zipstd::views::enumerate 不是什么黑魔法,它们只是把我们日常写循环时「手动做的事」封装成了标准组件。好处是代码更短、更安全、意图更明确。

我个人建议:下次你写 for 循环带下标,或者同时遍历多个容器时,先想想能不能用这两个视图。用习惯了之后,你会觉得手写下标循环反而别扭。

嗯,这一章就到这里。代码不多,但都是实战中高频用到的。你可以在自己的项目里试试,感受一下「视图思维」带来的变化。


公众号:蓝海资料掘金营,微信 deep3321