C++20 Ranges:全新范式
探索 C++20 Ranges──STL 算法接口的现代重新设计。学习范围概念、基于范围的算法、投影以及用于可组合数据变换的管道运算符。
为什么需要 Ranges?
C++20 Ranges 是对 STL 算法模型的根本性升级。传统 STL 要求向每个算法传递迭代器对,这既冗长又容易出错(你可能意外传递来自不同容器的迭代器)。Ranges 通过允许你直接传递容器本身──或任何定义了 begin() 和 end() 的对象来解决这个问题。
相比传统算法的关键改进:
1. 直接传递容器 ── std::ranges::sort(vec) 而不是 std::sort(vec.begin(), vec.end())
2. 投影 ── 在比较之前变换元素,而不修改它们
3. 概念 ── 算法有清晰的编译时要求
4. 可组合性 ── 使用管道运算符 | 链接操作
5. 惰性求值 ── 视图按需计算元素,避免中间副本
范围概念
C++20 在 中定义了一个范围概念层次结构:
- std::ranges::range ── 具有 begin() 和 end()
- std::ranges::sized_range ── 具有 O(1) 的 size()
- std::ranges::input_range ── 可以读取一次(类似输入迭代器)
- std::ranges::forward_range ── 多遍正向遍历
- std::ranges::bidirectional_range ── 正向 + 反向
- std::ranges::random_access_range ── 双向 + O(1) 跳跃
- std::ranges::contiguous_range ── 随机访问 + 连续内存
- std::ranges::common_range ── begin() 和 end() 返回相同类型
- std::ranges::viewable_range ── 可以转换为视图
这些概念用于约束基于范围的算法,当你传递不兼容的范围时会给出清晰的错误信息。
基于范围的算法
基于范围的算法位于 std::ranges:: 中,接受整个容器。它们还返回更丰富的结果类型,而不是裸迭代器。
#include <algorithm>
#include <ranges>
#include <vector>
#include <string>
#include <iostream>
struct Employee {
std::string name;
int age;
double salary;
};
int main() {
std::vector<int> nums = {5, 3, 1, 4, 1, 5, 9, 2, 6};
// Pass the container directly — no begin/end needed
std::ranges::sort(nums);
// nums: {1, 1, 2, 3, 4, 5, 5, 6, 9}
// find returns a borrowed iterator (safe with temporary ranges)
auto it = std::ranges::find(nums, 4);
if (it != nums.end()) {
std::cout << "Found: " << *it << '\n';
}
// count_if with ranges
auto even_count = std::ranges::count_if(nums, [](int x) { return x % 2 == 0; });
std::cout << "Even count: " << even_count << '\n'; // 3
// min/max on entire range
auto [min_it, max_it] = std::ranges::minmax_element(nums);
std::cout << "Min: " << *min_it << ", Max: " << *max_it << '\n';
// Sorting employees by salary (descending)
std::vector<Employee> team = {
{"Alice", 30, 95000},
{"Bob", 25, 72000},
{"Charlie", 35, 110000},
};
std::ranges::sort(team, std::ranges::greater{}, &Employee::salary);
// Charlie (110k), Alice (95k), Bob (72k)
for (const auto& e : team) {
std::cout << e.name << ": $" << e.salary << '\n';
}
}投影──比较前变换
投影是基于范围算法的独特功能。它们让你指定如何「查看」每个元素以进行比较或处理,而不修改元素本身。
#include <algorithm>
#include <ranges>
#include <vector>
#include <string>
#include <iostream>
#include <cctype>
int main() {
// Sort strings case-insensitively using a projection
std::vector<std::string> words = {"Banana", "apple", "Cherry", "date"};
std::ranges::sort(words, {}, [](const std::string& s) {
std::string lower = s;
std::transform(lower.begin(), lower.end(), lower.begin(),
[](char c) { return std::tolower(c); });
return lower;
});
// words: {"apple", "Banana", "Cherry", "date"}
// Original strings preserved, sorted by lowercase version
for (const auto& w : words) std::cout << w << ' ';
std::cout << '\n';
// Using member pointer as projection
struct Point { int x, y; };
std::vector<Point> points = {{3, 1}, {1, 5}, {2, 3}};
// Sort by x coordinate
std::ranges::sort(points, {}, &Point::x);
// points: {{1,5}, {2,3}, {3,1}}
// Find max by y coordinate
auto max_y = std::ranges::max_element(points, {}, &Point::y);
std::cout << "Max y: (" << max_y->x << ", " << max_y->y << ")\n"; // (1, 5)
// Binary search with projection
bool found = std::ranges::binary_search(points, 2, {}, &Point::x);
std::cout << std::boolalpha << "Has x=2: " << found << '\n'; // true
}管道运算符的组合
管道运算符 | 是 C++20 Ranges 的标志。它让你将范围适配器(视图)链接成一个从左到右阅读的管道,类似于 Unix shell 管道:
auto result = numbers
| std::views::filter([](int x) { return x % 2 == 0; })
| std::views::transform([](int x) { return x * x; })
| std::views::take(5);这读起来很自然:「取 numbers,保留偶数,将它们平方,取前 5 个。」该管道是惰性的──在你迭代 result 之前不会发生任何计算。这意味着不会分配中间 vector。
我们将在下一课深入探讨视图。
范围算法可以防止来自临时范围的悬空迭代器。如果你传递一个临时容器,返回的迭代器会悬空,因为临时对象在分号处被销毁:
auto it = std::ranges::find(get_vector(), 42);
// Returns std::ranges::dangling instead of an iterator!
// Attempting to dereference it won't compile.这是一个编译时安全特性──传统的 std::find 会静默地返回悬空迭代器,导致运行时的未定义行为。std::ranges::dangling 哨兵类型使其成为编译错误。
在新代码中优先使用 std::ranges:: 算法──它们更安全(无迭代器不匹配、悬空保护)、更可读(直接传容器)、更强大(投影、概念)。
在以下情况使用传统 std:: 算法:
- 你的代码库需要支持 C++20 之前的编译器
- 你需要 ranges 未提供的特定重载
- 你使用来自遗留 API 的迭代器对
注意:std::ranges:: 算法不是直接替换。某些算法返回不同的类型(例如,std::ranges::sort 返回排序范围的尾迭代器)。迁移时请查阅文档。
- C++20 Ranges 允许你直接将容器传给算法──不再需要
begin()/end()样板代码 - 投影在比较之前变换元素,而不修改原始数据
- 范围概念(
input_range、random_access_range等)提供清晰的编译时约束 - 管道运算符
|实现可组合的、从左到右的数据处理管道 std::ranges::dangling在编译时防止悬空迭代器 bug- 在新的 C++20 代码中优先使用
std::ranges::算法,以获得更好的安全性、可读性和功能
测验 — 检验你的知识
(20 经验值)1. C++20 范围算法中的投影是什么?
2. 用临时容器调用 `std::ranges::find` 会返回什么?
3. `std::ranges::sort(vec, std::ranges::greater{}, &Employee::salary)` 做了什么?