C++20引入的std::ranges库绝非简单的语法糖,而是对STL算法和迭代器体系的一次彻底重构。传统STL算法如std::sort需要传递首尾迭代器对,这种设计存在几个根本缺陷:首先,迭代器对无法自我验证有效性,容易产生首尾不匹配的错误;其次,算法组合时需要反复传递迭代器,导致代码冗余;最重要的是,缺乏对数据源的抽象,难以实现惰性求值和管道式操作。
std::ranges通过引入"范围(range)"这一核心概念解决了这些问题。一个range可以是任何具有begin()和end()的对象,包括标准容器、原生数组、生成器视图等。这种抽象使得算法可以直接作用于整个数据范围,而无需显式处理迭代器。更重要的是,它建立了统一的接口规范,使得算法组合和惰性求值成为可能。
std::ranges构建了一套精细的概念体系来约束不同类型的范围:
这些概念通过C++20的concept特性实现编译时约束。例如,std::ranges::sort要求随机访问范围且元素可交换:
cpp复制template <std::ranges::random_access_range R,
typename Comp = std::ranges::less,
typename Proj = std::identity>
requires std::sortable<std::ranges::iterator_t<R>, Comp, Proj>
constexpr std::ranges::borrowed_iterator_t<R> sort(R&& r, Comp comp = {}, Proj proj = {});
视图(view)是std::ranges最强大的特性之一,它们提供零成本的抽象转换。常见的视图包括:
| 视图工厂 | 功能描述 | 时间复杂度 |
|---|---|---|
| views::filter | 按谓词过滤元素 | O(1) |
| views::transform | 对每个元素应用转换函数 | O(1) |
| views::take | 取前N个元素 | O(1) |
| views::reverse | 反向遍历 | O(1) |
| views::join | 展平嵌套范围 | O(1) |
视图的惰性求值特性使得链式操作异常高效:
cpp复制auto result = data | views::filter(is_valid)
| views::transform(calculate)
| views::take(10);
这段代码不会立即执行任何计算,只有在迭代result时才会按需处理数据。
std::ranges提供了全套的约束版STL算法,它们相比传统版本有三大改进:
以std::ranges::find为例:
cpp复制std::vector<Person> people = /*...*/;
// 传统方式
auto it = std::find_if(people.begin(), people.end(),
[](const Person& p) { return p.age() > 30; });
// ranges方式
auto it = std::ranges::find_if(people,
[](int age) { return age > 30; },
&Person::age);
投影机制允许我们将注意力集中在要比较的属性上,而不必编写冗长的访问逻辑。
管道操作符(|)与视图的组合创造了声明式的数据处理流:
cpp复制// 处理日志文件的经典案例
void process_logs(std::string_view logfile) {
auto lines = get_lines(logfile)
| views::filter([](auto&& s) { return !s.empty(); })
| views::transform(parse_log_entry)
| views::filter(&LogEntry::is_error);
for (const auto& entry : lines | views::take(100)) {
handle_error(entry);
}
}
这种模式不仅更易读,而且由于视图的惰性特性,内存效率极高。
我们可以创建自己的视图适配器来扩展管道功能:
cpp复制template <std::ranges::input_range R>
auto chunk_view(R&& r, size_t n) {
return r | views::lazy_split(views::repeat(n) | views::take(std::ranges::size(r)/n + 1));
}
// 使用示例
for (auto&& chunk : data | chunk_view(64)) {
process_batch(chunk);
}
std::ranges算法天然支持组合使用:
cpp复制// 统计满足条件的元素
auto count = std::ranges::count_if(
data | views::filter(is_valid),
[](const auto& x) { return x.value() > threshold; });
// 查找并转换
if (auto it = std::ranges::find(data, target); it != data.end()) {
return transform(*it);
}
视图不拥有数据,因此必须注意底层数据的生命周期:
cpp复制auto get_filtered_data() {
std::vector<int> data = /*...*/;
// 危险:返回的视图将引用已销毁的data
return data | views::filter(predicate);
}
安全的做法是返回拥有数据的范围或立即物化视图:
cpp复制// 方案1:返回vector
auto get_filtered_data() {
std::vector<int> data = /*...*/;
auto filtered = data | views::filter(predicate);
return std::vector<int>(filtered.begin(), filtered.end());
}
// 方案2:使用views::all
auto get_filtered_data(std::vector<int> data) {
return views::all(data) | views::filter(predicate);
}
不同算法对范围有不同的复杂度保证:
| 算法 | 要求 | 复杂度保证 |
|---|---|---|
| ranges::sort | 随机访问范围 | O(N log N) |
| ranges::stable_sort | 随机访问范围 | O(N log N) |
| ranges::partial_sort | 随机访问范围 | O(N log K) |
| ranges::nth_element | 随机访问范围 | O(N)平均 |
| ranges::inplace_merge | 双向范围 | O(N) |
C++17的并行算法可以与ranges结合使用:
cpp复制std::vector<int> data = /*...*/;
std::sort(std::execution::par, data.begin(), data.end());
// ranges风格的并行排序
std::ranges::sort(std::execution::par, data);
当类型不满足算法要求时,编译器会输出复杂的概念检查错误。例如,尝试对单向链表排序:
cpp复制std::forward_list<int> lst = /*...*/;
std::ranges::sort(lst); // 错误:forward_list不是随机访问范围
解决方案是选择合适的容器或算法:
cpp复制// 方案1:转换为vector
std::vector<int> vec(lst.begin(), lst.end());
std::ranges::sort(vec);
// 方案2:使用适合前向迭代器的算法
std::ranges::copy(lst, std::ostream_iterator<int>(std::cout, " "));
视图迭代器依赖于原始数据,修改数据可能导致迭代器失效:
cpp复制std::vector<int> data = {1, 2, 3, 4};
auto even = data | views::filter([](int x) { return x % 2 == 0; });
auto it = even.begin(); // 指向2
data.push_back(6); // 可能导致vector重新分配
// 危险:it可能已失效
std::cout << *it << '\n';
安全的做法是在修改数据后重新获取迭代器。
投影机制有时会导致意外的重载解析问题:
cpp复制struct Point { int x, y; };
std::vector<Point> points = /*...*/;
// 错误:投影与lambda参数类型不匹配
std::ranges::sort(points, std::less{},
[](const Point& p) { return p.x; });
// 正确:明确lambda参数类型
std::ranges::sort(points, std::less<int>{},
[](const Point& p) { return p.x; });
创建返回范围对象的工厂函数,提高代码复用性:
cpp复制auto generate_sensor_data(int count) {
return std::views::iota(0, count)
| std::views::transform([](int i) {
return Sensor{i, random_reading()};
});
}
// 使用示例
for (const auto& sensor : generate_sensor_data(100)) {
monitor(sensor);
}
使用范围特性简化测试代码:
cpp复制TEST(RangesTest, FilterTransform) {
std::vector<int> input = {1, 2, 3, 4, 5};
auto result = input
| views::filter([](int x) { return x % 2 == 0; })
| views::transform([](int x) { return x * x; });
std::vector<int> expected = {4, 16};
ASSERT_TRUE(std::ranges::equal(result, expected));
}
C++20协程可以与范围视图结合,创建生成器:
cpp复制generator<int> fibonacci() {
int a = 0, b = 1;
while (true) {
co_yield a;
std::tie(a, b) = std::pair{b, a + b};
}
}
// 使用示例
for (int i : fibonacci() | views::take(10)) {
std::cout << i << ' ';
}