【C++23】(四)Ranges 增强与 views::chunk_by:数据处理新利器

数据处理时,你还在用循环 + 计数器分组吗?C++23 的 views::chunk_by 让「按条件对连续元素分组」变得优雅——一行代码搞定以前要 20 行的逻辑。


前言

C++20 引入了 Ranges 库,改变了我们写算法的方式:惰性求值、管道语法、组合性

C++23 在此基础上大幅增强,带来了一批实用的新 views:

  • chunk_by:按条件分组连续元素
  • enumerate:带索引遍历
  • zip_transform:多序列并行变换
  • concat:拼接视图
  • ranges::to:视图转容器

一、views::chunk_by:连续元素分组

1.1 痛点回顾

假设有这样一个需求:把数组 {1, 1, 2, 2, 2, 3, 1, 1} 按「相邻相同元素」分组

以前要这么写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
std::vector<int> nums = {1, 1, 2, 2, 2, 3, 1, 1};
std::vector<std::vector<int>> groups;
std::vector<int> current;

for (int n : nums) {
if (current.empty() || current.back() == n) {
current.push_back(n);
} else {
groups.push_back(current);
current.clear();
current.push_back(n);
}
}
if (!current.empty()) groups.push_back(current);

代码冗长,逻辑分散

1.2 chunk_by 的优雅解法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>
#include <vector>
#include <ranges>

namespace rv = std::views;

int main() {
std::vector<int> nums = {1, 1, 2, 2, 2, 3, 1, 1};

// views::chunk_by: 按谓词分组相邻元素 (C++23)
for (auto&& chunk : nums | rv::chunk_by(std::equal_to{})) {
int first = *chunk.begin();
int count = std::ranges::distance(chunk);
std::cout << "value=" << first << " count=" << count << "\n";
}
}

输出

1
2
3
4
value=1 count=2
value=2 count=3
value=3 count=1
value=1 count=2

1.3 chunk_by 的语义

chunk_by 的关键点:

  1. 比较相邻元素:用传入的谓词(如 std::equal_to{})判断相邻元素是否该分到同一组
  2. 只分组连续相同:非连续的相同值会分成不同组
  3. 返回 views:不复制数据,惰性求值
1
2
3
4
// chunk_by 的分组逻辑图示
// nums: {1, 1, 2, 2, 2, 3, 1, 1}
// ─── ───── ─ ───
// group1 group2 g3 g4

1.4 自定义谓词

可以用任意二元谓词,不只是相等比较:

1
2
3
4
5
6
7
// 按 "差值小于 3" 分组
std::vector<int> data = {1, 2, 4, 7, 8, 9, 12};
for (auto&& chunk : data | rv::chunk_by([](int a, int b) {
return b - a <= 2; // 差值小于等于2,认为是同一组
})) {
// chunk: {1,2}, {4}, {7,8,9}, {12}
}

二、views::enumerate:带索引遍历

2.1 痛点

以前想知道元素的下标,往往要额外维护一个计数器:

1
2
3
4
std::vector<std::string> words = {"hello", "world"};
for (size_t i = 0; i < words.size(); ++i) {
std::cout << i << ": " << words[i] << "\n";
}

2.2 enumerate 的优雅解法

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>
#include <vector>
#include <string>
#include <ranges>

namespace rv = std::views;

int main() {
std::vector<std::string> words = {"hello", "world"};
for (auto [i, word] : words | rv::enumerate) {
std::cout << i << ": " << word << "\n";
}
}

输出

1
2
0: hello
1: world

2.3 组合使用

enumerate 可以和其他 view 组合:

1
2
3
4
5
6
std::vector<int> nums = {3, 6, 9, 12, 15};

// 找第一个大于 10 的元素的索引
auto result = nums | rv::enumerate
| rv::filter([](auto&& item) { return item.second > 10; })
| rv::take(1);

三、views::zip_transform:多序列并行变换

3.1 痛点

想要把两个向量对应元素相加?以前要:

1
2
3
4
5
6
7
std::vector<int> a = {1, 2, 3};
std::vector<int> b = {10, 20, 30};
std::vector<int> result;

for (size_t i = 0; i < a.size(); ++i) {
result.push_back(a[i] + b[i]);
}

3.2 zip_transform 的优雅解法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>
#include <vector>
#include <functional>
#include <ranges>

int main() {
std::vector<int> a = {1, 2, 3};
std::vector<int> b = {10, 20, 30};

// zip_transform: 两两对应,经过变换
for (int sum : std::views::zip_transform(std::plus<>{}, a, b)) {
std::cout << sum << " "; // 11 22 33
}
std::cout << "\n";
}

3.3 组合自定义变换

1
2
3
4
5
6
7
8
9
10
// 两字符串对应字符拼接
std::vector<std::string> s1 = {"a", "b", "c"};
std::vector<std::string> s2 = {"1", "2", "3"};

for (auto&& combined : std::views::zip_transform(
[](const std::string& x, const std::string& y) {
return x + y;
}, s1, s2)) {
std::cout << combined << "\n"; // a1, b2, c3
}

四、views::concat:拼接视图

4.1 拼接多个容器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>
#include <vector>
#include <ranges>

int main() {
std::vector<int> a = {1, 2, 3};
std::vector<int> b = {4, 5};
std::vector<int> c = {6, 7, 8, 9};

for (int n : std::views::concat(a, b, c)) {
std::cout << n << " "; // 1 2 3 4 5 6 7 8 9
}
std::cout << "\n";
}

注意concat 返回的是 view,不复制数据,惰性求值。


五、std::ranges::to:视图转容器

5.1 痛点

Ranges 的 view 是惰性的,但最终往往需要转成具体容器:

1
2
3
// C++20 繁琐写法
auto filtered = nums | rv::filter([](int n) { return n % 2 == 0; });
std::vector<int> v(filtered.begin(), filtered.end()); // 显式构造

5.2 C++23 的 to

1
2
3
4
5
6
7
// C++23:一行搞定
auto v = nums | rv::filter([](int n) { return n % 2 == 0; })
| std::ranges::to<std::vector>();

// 甚至可以指定容器类型
auto s = nums | rv::transform([](int n) { return n * 2; })
| std::ranges::to<std::set>();

5.3 to 的工厂函数

1
2
3
// 更简洁的写法
auto v = std::ranges::to<vector>(nums | rv::filter(pred));
auto m = std::ranges::to<map<string, int>>(pairs | rv::transform(...));

六、完整示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <iostream>
#include <vector>
#include <ranges>
#include <map>

namespace rv = std::views;

int main() {
std::vector<int> nums = {1, 1, 2, 2, 2, 3, 1, 1};

// views::chunk_by: 按谓词分组相邻元素 (C++23)
for (auto&& chunk : nums | rv::chunk_by(std::equal_to{})) {
int first = *chunk.begin();
int count = std::ranges::distance(chunk);
std::cout << "value=" << first << " count=" << count << "\n";
}

// views::enumerate: 带索引遍历 (C++23)
std::vector<std::string> words = {"hello", "world"};
for (auto [i, word] : words | rv::enumerate) {
std::cout << i << ": " << word << "\n";
}

// views::zip_transform: 多序列并行变换
std::vector<int> a = {1, 2, 3};
std::vector<int> b = {10, 20, 30};
for (int sum : std::views::zip_transform(std::plus<>{}, a, b)) {
std::cout << sum << " "; // 11 22 33
}
std::cout << "\n";

// ranges::to: 视图转容器
auto v = nums | rv::filter([](int n) { return n % 2 == 0; })
| std::ranges::to<std::vector>();
(void)v;
}

七、C++23 Ranges 新特性一览

特性作用典型场景
views::chunk_by按谓词分组相邻元素数据分块、合并连续区间
views::enumerate带索引遍历需要下标的循环
views::zip_transform多序列并行变换向量运算、交叉合并
views::concat拼接多个视图多个容器串接
ranges::to<Container>视图转容器最终物化结果

行动建议:从今天起,遇到「分组」「索引」「两两运算」的场景,试试这些 C++23 Ranges 工具——你会发现数据处理代码可以如此简洁优雅。


📚 C++23 新特性 系列导航

本文是《C++23 新特性》系列第 4/4 篇。

方向章节
◀ 上一篇(三)std::print / to_underlying
📖 全部 4 篇目录(点击展开)
  1. (一)std::expected
  2. (二)if consteval 与 Deducing this
  3. (三)std::print / to_underlying
  4. (四)Ranges 增强 ← 当前

语法/控制流可视化

下面是一张马卡龙色 Mermaid 图,帮你从图形角度把握本章涉及的语法与控制流。

%%{init: {'theme': 'base', 'themeVariables': {'primaryColor': '#FFE5EC', 'primaryTextColor': '#5D5D5D', 'primaryBorderColor': '#FFB3C6', 'lineColor': '#B5EAD7', 'secondaryColor': '#C7CEEA', 'tertiaryColor': '#FFDAC1'}}}%%
flowchart LR
    SRC["📊 源数据<br/>vector / range"] --> V1["views::chunk_by<br/>相邻同值分组"]
    V1 --> V2["views::stride n<br/>等距采样"]
    V2 --> V3["views::enumerate<br/>附加下标"]
    V3 --> V4["views::zip<br/>多范围配对"]
    V4 --> COL["🪣 views::collect<br/>物化结果"]
    COL --> OUT["✅ 输出"]

    SRC -. "惰性求值" .- V1
    V1 -. "惰性" .- V2
    V2 -. "惰性" .- V3
    V3 -. "惰性" .- V4

    style SRC fill:#FFE5EC,stroke:#FFB3C6,color:#5D5D5D
    style V1 fill:#C7CEEA,stroke:#A8DADC,color:#5D5D5D
    style V2 fill:#B5EAD7,stroke:#A8DADC,color:#5D5D5D
    style V3 fill:#FFDAC1,stroke:#FFB3C6,color:#5D5D5D
    style V4 fill:#C7CEEA,stroke:#A8DADC,color:#5D5D5D
    style COL fill:#FFE5EC,stroke:#FFB3C6,color:#5D5D5D
    style OUT fill:#B5EAD7,stroke:#A8DADC,color:#5D5D5D

本章讲 C++23 Ranges 增强(含 views::chunk_by、views::stride 等),对比维度:本特性 vs C++20 ranges 旧写法 vs 其他语言数据切分。

对比分析

一、本特性 vs 旧写法

维度C++23 ranges 写法C++20 ranges 写法旧手写循环
按相邻同值分组v | chunk_by(eq)自己写组合 view双指针循环
等距采样v | stride(n)嵌套 filter / indices取模
与 zip 组合zip(v1, v2) | chunk_by(...)已有 zip嵌套迭代器

二、对比其他语言

语言数据切分 / 分组风格
C++23chunk_by / stride / zip惰性、view 组合
Pythonitertools.groupby / more-itertools解释、惰性
Rustchunks / group_by(slice / itertools)零开销
JavaStream Collectors.groupingBy急切
HaskellData.List.group / groupBy类型化惰性

三、优缺点

优点:

  • 把常见的”分组 / 采样”标准化为 view
  • 与现有 ranges 风格一致,学习成本低

缺点:

  • 部分 view 在不同编译器下性能差异较大
  • 命名约定仍在收敛

四、何时选

  • 任何”数据切片 + 分组”流水线:先看 C++23 views
  • 性能敏感:benchmark 后再决定是否回到手写循环