C++20 Ranges(上):惰性求值与组合式数据处理
C++进阶计划 · Day 10 | 预计学习时长:2小时
引言
为什么这个知识点重要
如果说 Day 9 的 Concepts 是 C++20 的"宪法",那么 Ranges 就是基于这部宪法构建的"城市"。Concepts 定义了什么是合格的"公民"(类型),而 Ranges 则利用这些概念构建了一套全新的数据处理范式------惰性求值(Lazy Evaluation)与组合式编程(Composition)。
在传统 C++ 中,处理数据需要这样写:
cpp
// 传统方式:每一步都立即计算,临时容器遍布
std::vector<int> nums = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
std::vector<int> evens, squares, results;
std::copy_if(nums.begin(), nums.end(), std::back_inserter(evens),
[](int n) { return n % 2 == 0; });
std::transform(evens.begin(), evens.end(), std::back_inserter(squares),
[](int n) { return n * n; });
std::transform(squares.begin(), squares.end(), std::back_inserter(results),
[](int n) { return n + 10; });
Ranges 让我们可以用数学般的优雅表达:
cpp
// Ranges 方式:管道式组合,惰性求值
auto results = nums
| std::views::filter([](int n) { return n % 2 == 0; })
| std::views::transform([](int n) { return n * n; })
| std::views::transform([](int n) { return n + 10; });
// 这里 transform 调用了两次是合法的,但可以合并为一次:
auto results2 = nums
| std::views::filter([](int n) { return n % 2 == 0; })
| std::views::transform([](int n) { return n * n + 10; });
与 Day 9 的关联
Day 9 的 Concepts 为 Ranges 提供了概念基础。Ranges 库大量使用 Concepts 来定义其核心抽象:
| Ranges 核心概念 | 基于 Day 9 的 Concepts |
|---|---|
std::ranges::range |
有 begin() 和 end() 的类型 |
std::ranges::view |
不拷贝、不持有数据的轻量范围 |
std::ranges::input_range |
可单遍遍历 |
std::ranges::viewable_range |
可转换为 view 的 range |
关键洞察:Concepts 是 C++20 的"类型系统革命",Ranges 则是这场革命在算法和数据结构领域的第一个重大应用。理解 Day 9 的 Concepts,将使你更深入理解 Ranges 的设计哲学。
与 Qt 实战场景的关联
对于 7 年 Qt 开发者,Ranges 在以下场景特别有用:
- Qt Model/View 数据过滤 :用
filter_view替代手动过滤QSortFilterProxyModel - 信号槽数据处理 :对
QList<int>等容器进行函数式处理 - QML 集成:在 C++ 层预处理数据,直接传递给 QML 列表视图
核心概念
1. Range/View 模型
1.1 什么是 Range
Range 是一个简化的抽象:任何可以用 begin() 和 end() 获取迭代器对的对象。
cpp
// Range 的本质:一个有 begin/end 的对象
template <typename T>
concept range = requires(T& t) {
std::ranges::begin(t); // 可获取开始迭代器
std::ranges::end(t); // 可获取结束迭代器
};
// 所有标准容器都是 Range
static_assert(std::ranges::range<std::vector<int>>); // ✓
static_assert(std::ranges::range<std::array<int, 10>>); // ✓
static_assert(std::ranges::range<std::string>); // ✓
static_assert(std::ranges::range<int[10]>); // ✓ 数组也是 Range!
1.2 什么是 View
View 是特殊的 Range,它具有两个关键特性:
- 不拥有数据:View 通常包装另一个 Range,不自行分配内存
- O(1) 拷贝/赋值:拷贝 View 非常廉价,因为数据不在 View 中
cpp
// View 的本质:不拥有数据、O(1) 拷贝
// C++20 标准中 view concept 的完整定义(简化)
template <typename T>
concept view =
std::ranges::range<T> &&
std::movable<T> &&
std::ranges::enable_view<T>; // 注意:enable_view 是一个变量模板
// 默认情况下,std::ranges::enable_view<T> 对于满足某些条件的类型为 true
// 更准确地说,标准中的定义还包括:
// - view 必须提供 begin()/end() 且复杂度为 O(1)
// - view 必须可拷贝或可移动(取决于具体类型)
//
// 实际 enable_view 的定义:
template <typename T>
inline constexpr bool enable_view =
std::derived_from<T, view_base> ||
(std::is_default_constructible_v<T> && std::is_move_constructible_v<T>);
// 对于 span、string_view 等类型,enable_view 被显式特化为 true
// 验证 view 的 O(1) 特性
static_assert(std::ranges::view<std::views::all_t<std::vector<int>&>>);
// 数组视图(不拥有数据)
static_assert(sizeof(std::span<int>) < sizeof(std::vector<int>)); // span 比 vector 小得多
Qt 关联 :可以类比为 Qt 的 QPointer 或 QWeakPointer------它们指向数据,但不拥有数据。
1.3 Range vs View 的关系
┌─────────────────────────────────────────────────────────────┐
│ Range (范围) │
│ 任何有 begin/end 的类型 │
│ - std::vector, std::list, std::array │
│ - 普通数组 int[] │
│ - 自定义容器 │
├─────────────────────────────────────────────────────────────┤
│ View (视图) │
│ Range 的特殊子集,不拥有数据 │
│ - std::span (数组视图) │
│ - std::ranges::filter_view │
│ - std::ranges::transform_view │
│ - std::ranges::drop_view │
└─────────────────────────────────────────────────────────────┘
2. 管道操作符 |
2.1 操作符重载机制
C++20 引入了 | 作为管道操作符,专门用于连接 View。这不是普通的重载,而是 Ranges 库的专门设计:
cpp
// 管道操作符的本质:view | adaptor
// 等价于:adaptor(view)
namespace std::ranges::views {
// 管道操作符的实际实现更复杂,以下是简化概念版:
// 实际上,operator| 的右侧参数是 view adaptor(函数对象),而非 view 本身
// std::views::filter 是一个函数对象,返回一个 filter_view
// auto evens = data | std::views::filter(pred);
// 等价于:auto evens = std::views::filter(pred)(data);
// 标准库中的实现大致如下(简化):
template <typename Rng, typename Adaptor>
requires std::ranges::viewable_range<Rng>
constexpr auto operator|(Rng&& rng, Adaptor&& adaptor) {
// Adaptor 是一个可调用对象,接受 range 并返回 view
return std::forward<Adaptor>(adaptor)(std::forward<Rng>(rng));
}
// 因此 filter(pred) 返回的 adaptor 会接收 data 并构造 filter_view
}
// 所以下面的写法完全等价:
auto v1 = std::views::filter(pred)(rng); // 直接调用
auto v2 = rng | std::views::filter(pred); // 管道操作符
2.2 链式调用
管道操作符支持无限链式组合:
cpp
std::vector<int> data = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
// 单管道
auto step1 = data | std::views::filter([](int n) { return n % 2 == 0; });
// 链式管道
auto step2 = data
| std::views::filter([](int n) { return n % 2 == 0; })
| std::views::transform([](int n) { return n * n; });
// 更长链式
auto result = data
| std::views::filter([](int n) { return n > 3; })
| std::views::drop(2) // 跳过前2个
| std::views::take(3) // 只取3个
| std::views::transform([](int n) { return n * 10; })
| std::views::reverse;
// 注意:drop 和 take 的参数必须是 std::ptrdiff_t 类型(或可隐式转换)
// 如果传入负数,行为是未定义的!
// std::views::drop(-1) // ❌ 未定义行为
// std::views::take(-1) // ❌ 编译错误(C++23 之前)
3. 惰性求值(Lazy Evaluation)
3.1 惰性 vs 立即求值
这是 Ranges 最重要的特性,也是与传统算法的本质区别:
cpp
std::vector<int> data = {1, 2, 3, 4, 5};
// 立即求值:transform_vector 立即遍历并生成结果
std::vector<int> eager = [&]{
std::vector<int> temp;
for (int n : data) {
temp.push_back(n * 2);
}
return temp;
}();
// 此时 eager 已经是 {2, 4, 6, 8, 10}
// 惰性求值:filter_view 不会立即执行,直到被遍历
auto lazy = data | std::views::filter([](int n) {
std::cout << "checking " << n << "\n"; // 不会立即输出!
return n % 2 == 0;
});
// 只有在这里真正遍历时,才会调用 lambda
std::cout << "--- First iteration ---\n";
for (int n : lazy) {
std::cout << "got " << n << "\n";
}
// 输出将是:
// --- First iteration ---
// checking 1
// checking 2
// got 2
// checking 3
// checking 4
// got 4
// checking 5
3.2 惰性求值的优势
cpp
// 场景:无限序列 + 惰性求值
// 这在传统 C++ 中是不可能的!
// 生成 0, 1, 2, 3, ... 无限序列(惰性)
auto naturals = std::views::iota(0);
// 永远不需要预先知道要多少个
auto first_100 = naturals | std::views::take(100); // 取前100个
auto evens = naturals | std::views::filter([](int n) {
return n % 2 == 0;
}); // 所有偶数(仍然惰性)
auto squares = evens | std::views::transform([](int n) {
return n * n;
});
// ⚠️ 注意:并非所有 view 都支持随机访问(operator[])
// 上述 squares 是一个 transform_view,它可能支持随机访问
// 但前提是底层 range(evens)支持随机访问
// 安全的方式:使用迭代器或范围 for 循环
for (int n : squares | std::views::take(10)) {
std::cout << n << " "; // 0 4 16 36 64 100 144 196 256 324
}
// 或者使用 std::ranges::advance 和迭代器
auto it = squares.begin();
for (int i = 0; i < 10; ++i, ++it) {
std::cout << *it << " ";
}
}
Qt 关联 :可以类比为 Qt 的 QML Connections------信号处理函数只有在信号发射时才执行,而不是"预先准备好所有结果"。
4. 常用 Views 详解
4.1 std::views::iota - 无限序列生成器
cpp
// iota(start) 生成从 start 开始的无限递增序列
auto naturals = std::views::iota(1); // 1, 2, 3, 4, ...
auto countdown = std::views::iota(10, 1); // 10, 9, 8, ... 1
auto steps = std::views::iota(0, 10); // 0, 1, 2, ... 9 (有限)
// 与 take 组合:生成指定数量
auto first_20 = std::views::iota(1) | std::views::take(20);
// 生成斐波那契数列(惰性)------ 正确方式
auto fibs_generator = []() {
return std::views::generate([a = 0, b = 1]() mutable {
auto result = a;
auto next = a + b;
a = b;
b = next;
return result;
});
};
// 使用
auto fibs = fibs_generator();
for (int n : fibs | std::views::take(10)) {
std::cout << n << " "; // 0 1 1 2 3 5 8 13 21 34
}
// ⚠️ 注意:std::views::generate 是 C++23 的特性
// 在 C++20 中,可以使用 iota 配合 transform 实现:
auto fibs_cpp20 = std::views::iota(0)
| std::views::transform([](int n) {
// 需要使用递归或预计算,无法用单个 transform 优雅实现
// 推荐使用 generate(C++23)或自定义 view
});
4.2 std::views::filter - 条件过滤
cpp
std::vector<int> data = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
// 过滤偶数
auto evens = data | std::views::filter([](int n) {
return n % 2 == 0;
});
// 过滤复杂的条件
auto filtered = data | std::views::filter([](int n) {
return n > 3 && n < 9 && n != 5;
});
// filter_view 是惰性的:每次迭代都调用谓词
// 这意味着如果你修改了 data[0],下次迭代 filter 可能会返回不同结果!
4.3 std::views::transform - 元素变换
cpp
std::vector<QString> qtStrings = {"Hello", "World", "Qt"};
// 字符串长度
auto lengths = qtStrings | std::views::transform([](const QString& s) {
return s.length();
}); // 返回 view,不复制字符串
// 平方计算
std::vector<int> nums = {1, 2, 3, 4, 5};
auto squared = nums | std::views::transform([](int n) {
return n * n;
}); // {1, 4, 9, 16, 25}
// 类型转换
auto asDouble = nums | std::views::transform([](int n) {
return static_cast<double>(n);
});
// 多重变换
auto complex = nums
| std::views::transform([](int n) { return n * 2; })
| std::views::transform([](int n) { return n + 1; });
4.4 std::views::take / drop - 数量限制
cpp
std::vector<int> data = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
// 取前5个
auto first5 = data | std::views::take(5); // {1, 2, 3, 4, 5}
// 跳过前3个
auto skip3 = data | std::views::drop(3); // {4, 5, 6, 7, 8, 9, 10}
// 组合:取中间5个 (skip 2, take 5)
auto middle = data | std::views::drop(2) | std::views::take(5); // {3, 4, 5, 6, 7}
// 无限序列 + take = 有限序列
auto tenSquares = std::views::iota(1)
| std::views::transform([](int n) { return n * n; })
| std::views::take(10); // 1, 4, 9, 16, 25, 36, 49, 64, 81, 100
4.5 std::views::reverse - 反向遍历
cpp
std::vector<int> data = {1, 2, 3, 4, 5};
// 反向遍历(不复制数据)
for (auto it = data.rbegin(); it != data.rend(); ++it) {
// ...
}
// 使用 reverse_view
for (int n : data | std::views::reverse) {
std::cout << n << " "; // 5 4 3 2 1
}
// 组合:反向后取前3个
auto last3 = data | std::views::reverse | std::views::take(3); // {5, 4, 3}
4.6 std::views::unique - 去重
cpp
std::vector<int> data = {1, 1, 2, 2, 2, 3, 1, 1};
std::vector<int> data = {1, 1, 2, 2, 2, 3, 1, 1};
// 去除相邻重复项(只在相邻元素重复时去重)
auto unique_nums = data | std::views::unique; // {1, 2, 3, 1}
// 注意:结果中的最后一个 1 是因为它和前一个 3 不重复
// ⚠️ 注意:std::views::unique 只处理相邻重复元素!
// 对于需要全局去重的场景,需要先排序
// 结合排序进行全局去重
std::vector<int> sorted_data = data;
std::ranges::sort(sorted_data);
auto sorted_unique = sorted_data | std::views::unique; // {1, 2, 3}
// ❌ 错误:下面的写法不会排序,只是 transform 无意义
// auto sorted_unique = data
// | std::views::transform([](int n) { return n; }) // 这只是复制,不排序
// | std::views::unique; // 结果仍然是 {1, 2, 3, 1}
4.7 std::span - 数组视图
cpp
// span 是一个不拥有数据的视图,类似于"指针+长度"
std::vector<int> vec = {1, 2, 3, 4, 5};
std::span<int> sp = vec; // 视图整个 vector
// 子视图
std::span<int> sub = sp.subspan(1, 3); // {2, 3, 4}
// 前/后元素
// first<3>() 和 last<2>() 需要编译期常量
auto first3 = sp.first<3>(); // ✅ 3 是编译期常量
auto last2 = sp.last<2>(); // ✅ 2 是编译期常量
// 如果大小是运行时变量,使用 first() 和 last()
int n = 3;
// auto first_n = sp.first<n>(); // ❌ 编译错误:n 不是编译期常量
auto first_n = sp.first(n); // ✅ 运行时版本
auto last_n = sp.last(n); // ✅ 运行时版本
// 或者使用 subspan
auto sub = sp.subspan(1, 3); // 运行时参数,{2, 3, 4}
// Qt 关联:类似于 QListView 或 QAbstractItemModel 的"视图"概念
// span 不拥有数据,只提供访问视角
代码实战
实战 1:Qt Model 数据预处理
cpp
#include <ranges>
#include <vector>
#include <iostream>
#include <QString>
#include <QDebug>
// 场景:Qt Model 中有一批用户数据,需要过滤、排序后展示
struct User {
QString name;
int age;
bool active;
};
std::vector<User> g_users = {
{"Alice", 25, true},
{"Bob", 17, true},
{"Charlie", 30, false},
{"Diana", 22, true},
{"Eve", 28, true},
{"Frank", 35, false},
{"Grace", 19, true},
};
// 过滤活跃用户
auto activeUsers = std::views::filter([](const User& u) {
return u.active;
});
// 过滤成年人(年龄 >= 18)
auto adults = std::views::filter([](const User& u) {
return u.age >= 18;
});
// 按年龄排序(用于展示)
// ⚠️ 注意:std::views::transform 不会排序!
// 它只是将每个元素转换为另一种类型,顺序保持不变
// 正确的排序方式:先收集到容器,然后使用 std::ranges::sort
auto processed = g_users
| activeUsers
| adults
| std::ranges::to<std::vector>(); // C++23:收集到容器
// 然后排序
std::ranges::sort(processed, [](const User& a, const User& b) {
return a.age < b.age;
});
// 最后转换输出
auto output = processed
| std::views::transform([](const User& u) {
return QString(" %1 years - %2")
.arg(u.age)
.arg(u.name);
});
// 如果不想使用 C++23 的 to_vector,可以使用:
std::vector<User> processed_vec;
std::ranges::copy(g_users | activeUsers | adults, std::back_inserter(processed_vec));
std::ranges::sort(processed_vec, /* comparator */);
// Qt 中使用
void displayProcessedUsers() {
qDebug() << "Active adult users (sorted by age):";
for (const QString& line : processedUsers) {
qDebug() << line;
}
// 输出:
// 19 years - "Grace"
// 22 years - "Diana"
// 25 years - "Alice"
// 28 years - "Eve"
}
逐行解读:
cpp
// 第1步:定义过滤器
auto activeUsers = std::views::filter([](const User& u) {
return u.active; // 只保留 active == true 的用户
});
// 此时只是创建了一个惰性视图,不做任何计算
// 第2步:链式组合
auto processedUsers = g_users | activeUsers | adults | sortedByAge;
// | 操作符将前面的 view 传递给下一个 adaptor
// 等价于: adults(sortedByAge(activeUsers(g_users)))
// 但更易读、更符合数据流思维
// 第3步:迭代时真正执行
for (const QString& line : processedUsers) {
// 此时才会依次执行:
// 1. 遍历 g_users
// 2. 对每个元素应用 activeUsers 过滤器
// 3. 对结果应用 adults 过滤器
// 4. 对结果打包为 pair
// 5. 转换为 QString
}
实战 2:实现自定义 View Adaptor
cpp
#include <ranges>
#include <vector>
#include <iostream>
#include <algorithm>
// 需求:创建一个 view,将奇数翻倍,偶数保持不变
// Step 1: 定义适配器闭包
// 使用 decltype(auto) 显式保留返回类型
auto double_odds = []<std::ranges::viewable_range R>(R&& r)
-> decltype(auto) {
return r | std::views::transform([](int n) {
return (n % 2 != 0) ? n * 2 : n;
});
};
// 或者使用 C++20 的 auto 返回类型推导(也可以,但某些情况下可能失败)
auto double_odds_v2 = []<std::ranges::viewable_range R>(R&& r) {
return r | std::views::transform([](int n) {
return (n % 2 != 0) ? n * 2 : n;
});
};
// 为了确保返回类型是 view,可以使用 std::ranges::viewable_range 约束
// 并且确保输入类型满足 viewable_range
// Step 2: 绑定到 std::views(可选,让语法更统一)
namespace views = std::views;
// 测试
int main() {
std::vector<int> data = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
auto result = data
| double_odds
| views::filter([](int n) { return n > 5; }) // 过滤大于5的
| views::take(3); // 只取前3个
std::cout << "Result: ";
for (int n : result) {
std::cout << n << " "; // 输出: 6 10 14
}
std::cout << "\n";
// 详细分解执行过程:
// 原始: {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
// double_odds: {2, 2, 6, 4, 10, 6, 14, 8, 18, 10}
// filter(>5): {6, 10, 6, 14, 8, 18, 10}
// take(3): {6, 10, 6}
return 0;
}
逐行解读:
cpp
// 自定义 adaptor 的关键是返回一个 view
auto my_adaptor = []<std::ranges::viewable_range R>(R&& r) {
// R&& 是转发引用,可以接受左值和右值
// viewable_range 确保输入可以被转换为 view
// 返回值是 transform_view,它也是一个 view
return r | std::views::transform([](int n) {
return n * 2; // 具体变换逻辑
});
};
// 使用时与其他 views 无缝组合
auto combined = data | my_adaptor | std::views::filter(pred);
实战 3:Ranges 与算法结合
cpp
#include <ranges>
#include <algorithm>
#include <vector>
#include <numeric>
#include <iostream>
int main() {
std::vector<int> data = {3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5};
// 传统方式:需要临时容器
std::vector<int> temp1, temp2, temp3;
std::copy_if(data.begin(), data.end(), std::back_inserter(temp1),
[](int n) { return n > 3; });
std::transform(temp1.begin(), temp1.end(), std::back_inserter(temp2),
[](int n) { return n * n; });
std::sort(temp2.begin(), temp2.end());
auto result1 = std::accumulate(temp2.begin(), temp2.end(), 0);
// Ranges 方式:一行表达式,惰性求值
auto result2 = data
| std::views::filter([](int n) { return n > 3; })
| std::views::transform([](int n) { return n * n; })
| std::views::common; // 转换为支持普通迭代器的视图
// ⚠️ std::ranges::to_vector 是 C++23 特性(P1206R7)
// 在 C++20 中不可用!
// C++20 兼容方式:
std::vector<int> vec;
std::ranges::copy(
data
| std::views::filter([](int n) { return n > 3; })
| std::views::transform([](int n) { return n * n; }),
std::back_inserter(vec)
);
// 或者使用 ranges::to(C++23 通用版本):
// auto vec = data | ranges::to<std::vector<int>>(); // 需要 C++23
// Ranges + 标准算法
auto processed = data
| std::views::filter([](int n) { return n % 2 == 1; }) // 奇数
| std::views::transform([](int n) { return n * n; }) // 平方
| std::views::drop(1); // 跳过第一个
// 使用 std::ranges 算法
auto min_elem = std::ranges::min(processed);
auto max_elem = std::ranges::max(processed);
// ⚠️ 注意:std::ranges::fold_left 是 C++23 的特性!
// 在 C++20 中,可以使用 std::accumulate 或 std::ranges::for_each
// C++20 兼容方式:
auto sum = std::accumulate(processed.begin(), processed.end(), 0);
// 或者使用 std::ranges::for_each(C++20)
int sum = 0;
std::ranges::for_each(processed, [&sum](int n) { sum += n; });
// C++23 方式(需要 <numeric> 头文件)
// #include <numeric>
// auto sum = std::ranges::fold_left(processed, 0, std::plus<>{});
std::cout << "Min: " << min_elem << "\n";
std::cout << "Max: " << max_elem << "\n";
std::cout << "Sum: " << sum << "\n";
// 验证结果
// 原始奇数: 3, 1, 5, 9, 5, 3, 5
// 平方: 9, 1, 25, 81, 25, 9, 25
// 跳过第一个: 1, 25, 81, 25, 9, 25
// Min: 1, Max: 81, Sum: 166
return 0;
}
关键 API 解读:
cpp
// std::ranges::min/max - 范围版本的极值查询
auto min_elem = std::ranges::min(view);
auto max_elem = std::ranges::max(view);
// std::ranges::fold_left - C++23 的折叠(C++20 可用 accumulate)
auto sum = std::ranges::fold_left(view, 0, std::plus<>{});
// std::views::common - 转换为 common_range
// 某些视图(如 iota)迭代器类型不同,需要转换才能用于某些算法
auto common_view = infinite | std::views::take(100) | std::views::common;
// std::ranges::to_vector (C++23) - 直接收集到容器
auto vec = view | std::ranges::to_vector;
实战 4:处理 Qt 容器
cpp
#include <ranges>
#include <QList>
#include <QString>
#include <QDebug>
// Qt 容器天然支持 std::ranges,因为 QList 提供了 begin/end
void processQtContainers() {
QList<QString> names = {"Alice", "Bob", "Charlie", "Diana", "Eve"};
// 过滤长度大于4的名字
auto longNames = names
| std::views::filter([](const QString& s) {
return s.length() > 4;
});
// 转换为大写
auto upperNames = longNames
| std::views::transform([](const QString& s) {
return s.toUpper();
});
qDebug() << "Long names (uppercase):";
for (const QString& name : upperNames) {
qDebug() << " " << name;
}
// 输出: "CHARLIE" "DIANA"
// 与 QML 列表视图集成
// 假设你有一个 QStringList 需要传递给 QML
QStringList qmlList;
for (const QString& name : upperNames) {
qmlList << name;
}
// 只需要遍历一次,惰性求值确保内存效率
// 组合多个 Qt 容器
QList<int> ages = {25, 17, 30, 22, 28};
QList<QString> filteredWithAges;
// ⚠️ std::views::zip_transform 是 C++23 特性!
// 在 C++20 中不可用
// C++20 兼容方式:使用 boost::zip 或手动迭代
std::vector<QString> zipped;
auto it1 = names.begin();
auto it2 = ages.begin();
for (; it1 != names.end() && it2 != ages.end(); ++it1, ++it2) {
zipped.push_back(QString("%1 (%2)").arg(*it1).arg(*it2));
}
// 或者使用 C++23 的 zip_transform 需要 <ranges> 头文件支持
// #include <ranges>
// auto zipped = std::views::zip_transform(
// [](const QString& name, int age) {
// return QString("%1 (%2)").arg(name).arg(age);
// },
// names, ages
// );
for (const QString& info : zipped) {
qDebug() << info;
}
}
// 技巧:使用 views::iota 生成 QList 索引
void indexedProcessing() {
QList<QString> items = {"A", "B", "C", "D", "E"};
// 注意:transform 中的 items[i] 每次迭代都会执行索引访问
// 对于 QList,operator[] 是 O(1) 的,所以没问题
auto indexed = std::views::iota(0)
| std::views::take(items.size())
| std::views::transform([&](int i) {
return QString("[%1]: %2").arg(i).arg(items[i]); // QList 的 operator[] O(1)
});
// 但需要注意:如果 items 在 transform 执行期间被修改,行为是未定义的
// 因为 transform_view 是惰性的,items 的修改会反映在结果中
// 更安全的方式:先复制索引或使用 QList 的迭代器
auto indexed_safe = std::views::iota(0)
| std::views::take(items.size())
| std::views::transform([items = items](int i) { // 捕获副本
return QString("[%1]: %2").arg(i).arg(items[i]);
});
for (const QString& item : indexed) {
qDebug() << item;
}
}
常见陷阱与最佳实践
陷阱 1:View 被销毁后继续使用迭代器
cpp
// ❌ 危险:返回指向临时视图的迭代器
auto getEvens() {
std::vector<int> data = {1, 2, 3, 4, 5};
auto evens = data | std::views::filter([](int n) { return n % 2 == 0; });
return evens.begin(); // 危险!evens 在函数结束时销毁
}
// ✅ 正确:返回容器或使用 span
auto getEvens() -> std::vector<int> {
std::vector<int> data = {1, 2, 3, 4, 5};
std::vector<int> result;
for (int n : data | std::views::filter([](int n) { return n % 2 == 0; })) {
result.push_back(n);
}
return result; // 返回副本
}
// ❌ 不可行:filter_view 不能直接转为 span
// 因为 span 要求数据是连续存储的,而 filter_view 是惰性的
// ✅ 正确:返回实际容器(或使用 shared_ptr 等)
std::vector<int> getEvens(const std::vector<int>& data) {
std::vector<int> result;
for (int n : data | std::views::filter([](int n) { return n % 2 == 0; })) {
result.push_back(n);
}
return result; // NRVO/RVO 优化,零拷贝
}
// ✅ 正确:接受输出迭代器(类似标准库风格)
template <typename OutputIt>
void getEvens(const std::vector<int>& data, OutputIt out) {
for (int n : data | std::views::filter([](int n) { return n % 2 == 0; })) {
*out++ = n;
}
}
陷阱 2:惰性求值导致意外行为
cpp
std::vector<int> data = {1, 2, 3, 4, 5};
// ❌ 错误:每次迭代都重新应用所有转换
auto bad = data | std::views::filter([](int n) {
std::cout << "filter " << n << "\n";
return n % 2 == 0;
});
// 如果多次遍历同一视图,filter 会被调用多次
for (int n : bad) std::cout << "got " << n << "\n"; // filter 被调用5次
for (int n : bad) std::cout << "got " << n << "\n"; // filter 又被调用5次!
// ✅ 正确:先物化(materialize)到容器
std::vector<int> materialized;
std::ranges::copy(bad, std::back_inserter(materialized));
// C++23 更简洁的方式:
// auto materialized = bad | std::ranges::to<std::vector<int>>();
// ⚠️ 注意:物化会消耗额外的内存,适用于:
// - 需要多次遍历同一数据
// - 需要随机访问
// - 需要在不同线程间传递数据
//
// 如果只需要单次遍历,直接使用 view 更高效(零拷贝)
for (int n : materialized) std::cout << "got " << n << "\n"; // 只计算一次
for (int n : materialized) std::cout << "got " << n << "\n"; // 使用缓存
陷阱 3:无限视图导致的死循环
cpp
// ❌ 危险:iota 默认生成无限序列
auto naturals = std::views::iota(0); // 无限序列!
// 这些不会终止(除非 take/break)
// auto evens = naturals | std::views::filter([](int n) { return n % 2 == 0; });
// for (int n : evens) { } // 死循环!
// ✅ 正确:总是用 take 或 break 限制
auto first100 = std::views::iota(0) | std::views::take(100);
// ✅ 正确:使用 break 条件退出
for (int n : naturals) {
if (n > 1000) break; // 手动终止
}
陷阱 4:View 不能直接用于需要容器的场景
cpp
std::vector<int> data = {3, 1, 4, 1, 5, 9, 2, 6};
// ❌ 错误:view 没有 .size() 方法(某些 view 支持 size())
auto filtered = data | std::views::filter([](int n) { return n > 3; });
// size_t len = filtered.size(); // 编译错误!filter_view::size() 需要 O(1)
// filtered.size() 可能不存在或需要 O(n) 计算
// ✅ 正确:使用 ranges::distance
size_t len = std::ranges::distance(filtered); // 可以工作,但可能 O(n)
// ✅ 更好:需要 size 时物化到容器
std::vector<int> materialized(filtered.begin(), filtered.end());
size_t len = materialized.size(); // O(1)
陷阱 5:管道操作符优先级
cpp
std::vector<int> data = {1, 2, 3, 4, 5};
// ⚠️ 注意:管道操作符是左结合的
// data | filter(pred) | transform(fn) | take(3)
// 解析为:(((data | filter(pred)) | transform(fn)) | take(3))
// 这个解析顺序是符合预期的,不需要额外括号
// 真正需要注意的优先级问题:
// ❌ 错误:在复杂表达式中,函数调用优先级高于管道
auto wrong = data | std::views::filter([](int n) { return n > 2; })
| std::views::transform([](int n) { return n * 2; })
| std::views::take(3 + 1);
// 这里的 3 + 1 会先计算为 4,所以结果是 take(4)
// 这不会导致语法问题,但可读性差
// ✅ 正确:使用括号使意图更清晰
auto correct = data
| std::views::filter([](int n) { return n > 2; })
| std::views::transform([](int n) { return n * 2; })
| std::views::take(4);
// ✅ 正确:使用括号明确优先级
auto correct = data
| std::views::filter([](int n) { return n > 2; })
| std::views::transform([](int n) { return n * 2; })
| std::views::take(3); // take(3),明确
// ✅ 推荐:复杂表达式提取为变量
auto isEven = [](int n) { return n % 2 == 0; };
auto doubleIt = [](int n) { return n * 2; };
auto positive = [](int n) { return n > 0; };
auto result = data | std::views::filter(isEven)
| std::views::transform(doubleIt)
| std::views::filter(positive);
最佳实践 1:View 组合命名
cpp
// ✅ 好的实践:为复杂管道定义语义化名称
auto getAdultUsers = std::views::filter([](const User& u) {
return u.age >= 18;
});
auto getActiveUsers = std::views::filter([](const User& u) {
return u.active;
});
auto sortByAge = std::views::transform([](const User& u) {
return std::pair{u.age, u.name};
});
// 业务逻辑组合
auto adultActiveUsersSorted = g_users
| getAdultUsers
| getActiveUsers
| sortByAge;
最佳实践 2:调试惰性视图
cpp
// ✅ 添加日志的技巧
auto debugView = [](auto view, const char* name) {
return view | std::views::transform([=](auto&& elem) {
std::cout << name << ": " << elem << "\n";
return std::forward<decltype(elem)>(elem);
});
};
// 使用
auto result = data
| debugView(std::views::filter(pred), "filter")
| debugView(std::views::transform(fn), "transform")
| std::views::take(10);
进阶思考
1. View 的复杂度保证
Ranges 库对 View 有严格的复杂度保证:
| 操作 | 复杂度要求 |
|---|---|
| 构造 View | O(1) |
| 拷贝/赋值 View | O(1) |
begin() / end() |
O(1) |
size() (如果支持) |
O(1) |
这确保了 View 的"零拷贝、零抽象惩罚"特性。
2. View 与算法复杂度的关系
cpp
// Ranges 不会改变算法的复杂度
// filter + transform 仍然是 O(n)
// 但可以优化内存使用:
// 传统:需要多个临时容器
std::vector<int> temp1, temp2, temp3;
for (int n : data) if (pred1(n)) temp1.push_back(n);
for (int n : temp1) temp2.push_back(fn1(n));
for (int n : temp2) if (pred2(n)) temp3.push_back(n);
// 空间复杂度: O(3n)
// Ranges:单次遍历,多个适配器组合
auto result = data
| std::views::filter(pred1)
| std::views::transform(fn1)
| std::views::filter(pred2);
// 空间复杂度: O(1) - 不需要临时容器
// 时间复杂度: O(n) - 单次遍历
3. 自定义 View Adaptor 的完整模板
cpp
#include <ranges>
#include <concepts>
#include <iterator>
// 注意:自定义 view 类通常需要继承 view_interface 以自动实现
// size()、empty()、data() 等成员函数
// Step 1: 定义自定义 view 类
template <std::ranges::viewable_range V>
requires std::ranges::view<V> // 要求 V 本身是 view
class my_view : public std::ranges::view_interface<my_view<V>> {
private:
V base_; // 底层 range(必须是 view)
public:
// 默认构造
my_view() requires std::default_initializable<V> = default;
// 从 range 构造(接收 view)
constexpr explicit my_view(V base) : base_(std::move(base)) {}
// 基 range 访问
constexpr V base() const& requires std::copyable<V> { return base_; }
constexpr V base() && { return std::move(base_); }
// 迭代器类型(需要实现)
struct iterator {
// ... 迭代器实现
};
// begin/end
constexpr auto begin() { /* 返回迭代器 */ }
constexpr auto end() { /* 返回哨兵 */ }
};
// Step 2: 定义 adaptor 函数对象
struct my_adaptor {
template <std::ranges::viewable_range R>
constexpr auto operator()(R&& r) const {
// 确保传入的是 view
auto view = std::views::all(std::forward<R>(r));
return my_view<decltype(view)>(std::move(view));
}
};
// Step 3: 绑定到 std::views 命名空间(可选)
namespace std::views {
inline constexpr my_adaptor my;
}
4. Ranges 对 C++ 标准库的影响
cpp
// C++20 起,所有算法都有 ranges 版本
namespace std::ranges {
// 算法<算法名>
template <range R, class T>
constexpr T find(R&& r, const T& value);
template <range R, class P>
constexpr std::ranges::iterator_t<R> find_if(R&& r, P pred);
template <range R, class T>
constexpr iota_result min(R&& r, T value);
// 还有更多...
}
// 使用新算法
auto it = std::ranges::find(container, target);
auto it = std::ranges::find_if(container, pred);
// ranges 算法可以直接接受 view,不需要 begin/end
auto it = std::ranges::find(data | filter_view, target);
参考资源
标准文档
权威书籍
- 《C++20 Ranges Guide》 - Barry Revzin (Ranges 库设计者之一)
- 《Modern C++ Programming Cookbook》 - 涵盖 Ranges 实战
- 《Mastering the C++17 STL》 - 为学习 Ranges 打基础
高质量博客
- Barry Revzin's Blog - Ranges 设计者的博客
- Fluent C++ - C++20 Ranges Guide
- Simon Brand - Understanding C++ Views
- Kostas Kyrimis - Ranges are Monads
Qt 集成资源
- Qt Containers 与 Ranges - Qt 容器天然支持 ranges
- C++20 in Qt 6.2+ - Qt 对现代 C++ 的支持
下期预告:Day 11 - C++20 Ranges(下):视图工厂、视图转换、算法与Ranges结合、C++23 Ranges 新特性