1. 问题起源:为什么需要 Ranges
传统 STL 算法使用迭代器对 [begin, end) 表达范围,这种方式存在三个根本性问题:
1.1 迭代器对分离是万恶之源
cpp
// 传统写法:begin/end 必须成对传递,容易出错
std::vector<int> v = {1, 2, 3, 4, 5, 6, 7, 8};
auto it = std::find_if(v.begin(), v.end(), [](int x) { return x > 5; });
// 稍有不慎就会写出:
// std::find_if(v.begin(), w.end(), pred); // 不同容器的迭代器,UB!
// Ranges 写法:范围是一个整体
auto it = std::ranges::find_if(v, [](int x) { return x > 5; });
1.2 组合操作需要中间容器
cpp
// 传统写法:每一步都需要中间容器,产生大量临时分配
std::vector<int> temp1, temp2, result;
std::copy_if(v.begin(), v.end(), std::back_inserter(temp1),
[](int x) { return x % 2 == 0; });
std::transform(temp1.begin(), temp1.end(), std::back_inserter(temp2),
[](int x) { return x * x; });
std::copy_n(temp2.begin(), 4, std::back_inserter(result));
// 三次内存分配,大量数据拷贝
// Ranges 写法:零开销组合,一次遍历完成
auto result = v | std::views::filter([](int x) { return x % 2 == 0; })
| std::views::transform([](int x) { return x * x; })
| std::views::take(4);
// 零中间分配,惰性求值
1.3 排序必须传递整个范围
cpp
// 传统写法:无法对部分范围排序而不影响原容器
std::sort(v.begin(), v.end()); // 只能全排
// Ranges 写法:投影(Projection)按成员排序,无需手写比较器
struct Person { std::string name; int age; };
std::vector<Person> people = {{"Alice", 30}, {"Bob", 25}, {"Charlie", 35}};
std::ranges::sort(people, {}, &Person::age);
// 范围 比较器(默认<) 投影:按age字段排序
|------|-----------------|---------------------|
| 维度 | 传统 STL | C++20 Ranges |
| 范围表达 | begin/end 迭代器对 | 单一 range 对象 |
| 组合方式 | 中间容器 + 多次遍历 | 管道运算符 + 惰性求值 |
| 比较器 | 手写 lambda 或函数对象 | 投影 projection 简化 |
| 安全检查 | 无(迭代器失效是 UB) | borrowed_range 概念约束 |
| 类型推导 | 复杂的迭代器类型 | auto + Concept 约束 |
2. 核心概念:Range / View / Sentinel / Projection
2.1 std::ranges::range 概念
cpp
// range 概念的核心定义(简化版)
template<typename T>
concept range = requires(T& t) {
std::ranges::begin(t); // 必须有 begin
std::ranges::end(t); // 必须有 end
};
// 注意:begin 和 end 可以返回不同类型!(sized_sentinel)
任何满足了 begin() 和 end() 的类型都是 range。与经典 STL 的关键区别是:begin 和 end 的返回类型可以不同,这催生了 sentinel(哨兵)概念。
2.2 View:轻量级的 Range 包装器
cpp
// View 是 O(1) 拷贝/移动/析构的 range
template<typename T>
concept view = range<T> && std::movable<T> && std::default_initializable<T>;
// View 的语义保证:拷贝视图不拷贝底层数据
auto v1 = std::views::iota(0, 100); // iota_view<int>
auto v2 = v1; // O(1) 拷贝,v2 是 v1 的独立副本
// 两者可以独立迭代,互不影响
View 的设计精髓:View 本身存储的是"如何生成元素"的逻辑,而非元素本身。拷贝 View 仅仅是拷贝迭代状态,不触发底层数据的任何拷贝。
2.3 Sentinel:非对称范围的终点
cpp
// 经典示例:C风格字符串
const char* str = "hello world";
// 传统写法:需要先 strlen 才能构造 end 迭代器
auto traditional_end = str + strlen(str); // 必须先知道长度
// Ranges:sized_sentinel 允许 begin 和 end 类型不同
// std::unreachable_sentinel_t 可以表示"无限范围"
auto sentinel = std::unreachable_sentinel; // 永不等于任何迭代器
// 实际用例:以 '\0' 为终止条件
auto cstr_view = std::ranges::subrange(str, std::unreachable_sentinel);
// begin(char*) end(哨兵)
2.4 Projection:排序 / 查找的无侵入投影
cpp
// 投影是 C++20 Ranges 最被低估的特性
std::vector<std::string> words = {"apple", "banana", "Cherry", "Date"};
// 按字符串长度排序,大小写不敏感查找
std::ranges::sort(words, {}, &std::string::size);
// 范围 比较器 投影:按长度排序
auto it = std::ranges::find(words, "cherry",
[](char a, char b) { return std::tolower(a) == std::tolower(b); },
[](const std::string& s) { return s; } // 投影:identity
);
// 等价于先对每个元素调用 projection,再传给比较器
投影的底层实现:
cpp
// 标准库的 invoke 投影机制(模拟实现)
template<typename Pred, typename Proj>
struct projected_predicate {
Pred pred;
Proj proj;
template<typename T>
constexpr bool operator()(T&& val) const {
return std::invoke(pred, std::invoke(proj, std::forward<T>(val)));
}
};
// 每次比较前先调用 proj 提取关键字段,再交给 pred 比较
3. 视图适配器的实现原理
视图适配器(View Adaptor)是 Ranges 的核心组件。以 filter_view 为例,逐层剖析其实现。
3.1 filter_view 的完整骨架
cpp
// 标准库 filter_view 的简化实现(GCC libstdc++ 风格)
template<std::input_range V, std::indirect_unary_predicate<iterator_t<V>> Pred>
requires view<V>
class filter_view : public std::ranges::view_interface<filter_view<V, Pred>> {
private:
V base_; // 底层视图
Pred pred_; // 过滤谓词(使用 [[no_unique_address]] 优化空谓词)
public:
// 迭代器类 ------ 核心复杂度所在
class iterator {
private:
iterator_t<V> current_; // 当前位置
iterator_t<V> end_; // 底层范围的结束
const filter_view* parent_; // 反向指针,访问谓词
// 关键方法:跳过不满足谓词的元素
void satisfy() {
while (current_ != end_ && !std::invoke(parent_->pred_, *current_))
++current_;
}
public:
using iterator_category = std::bidirectional_iterator_tag;
using value_type = std::iter_value_t<iterator_t<V>>;
using difference_type = std::iter_difference_t<iterator_t<V>>;
iterator() = default;
iterator(filter_view& parent, iterator_t<V> current)
: current_(std::move(current))
, end_(std::ranges::end(parent.base_))
, parent_(&parent)
{
satisfy(); // 构造时立即跳到第一个满足条件的元素
}
decltype(auto) operator*() const { return *current_; }
iterator& operator++() {
++current_;
satisfy(); // 每次前进后重新定位
return *this;
}
iterator operator++(int) {
auto tmp = *this;
++*this;
return tmp;
}
// 双向范围支持 --
iterator& operator--() {
do {
--current_;
} while (!std::invoke(parent_->pred_, *current_));
return *this;
}
bool operator==(const iterator& other) const {
return current_ == other.current_;
}
// 关键:和底层范围的 sentinel 比较
bool operator==(std::default_sentinel_t) const {
return current_ == end_;
}
friend class filter_view;
};
// begin() ------ 需要找到第一个满足条件的元素
constexpr auto begin() {
return iterator{*this, std::ranges::begin(base_)};
}
// end() ------ 返回哨兵
constexpr auto end() {
return std::default_sentinel;
}
};
3.2 satisfy() 的 O(N) 隐患与缓存策略
filter_view::iterator 的 satisfy() 在每次 ++ 时线性扫描,导致以下场景出现性能下降:
cpp
std::vector<int> v(1'000'000);
std::iota(v.begin(), v.end(), 1);
auto even = v | std::views::filter([](int x) { return x % 2 == 0; });
// 每次 ++ 都要扫描到下一个偶数
// 遍历 50 万个元素时,satisfy() 累计比较约 100 万次(每个元素判断两次)
// 但因为每次 ++ 只需要前进 1~2 步找到下一个偶数,实际开销仍然可控
优化策略 :对稠密谓词(大部分元素满足),filter_view 开销近似 O(1);对稀疏谓词(极少数元素满足),++ 的摊还成本升高。如果大量随机访问,优先使用 views::take_while + 索引访问替代 filter_view。
3.3 transform_view 的零开销抽象
cpp
template<std::input_range V, std::copy_constructible F>
requires view<V>
class transform_view : public view_interface<transform_view<V, F>> {
private:
V base_;
[[no_unique_address]] F fun_; // 空 lambda 不占空间
class iterator {
iterator_t<V> current_;
const transform_view* parent_;
public:
// transform 的核心:operator* 时才调用变换函数
decltype(auto) operator*() const {
return std::invoke(parent_->fun_, *current_);
}
iterator& operator++() {
++current_;
return *this; // 注意:没有 satisfy!
}
// ...
};
};
|-------|-----------------|----------------|
| 特性 | filter_view | transform_view |
| ++ 开销 | O(到下一个满足条件的距离) | O(1) |
| * 开销 | O(1) | O(F 的调用开销) |
| 随机访问 | 不支持 | 条件支持(若 V 支持) |
| 已知大小 | 不支持 sized_range | 支持(继承 V 的大小) |
| 适合场景 | 筛选子集 | 值映射 |
3.4 take_view 与 drop_view:窗口操作
cpp
// take_view:取前 N 个元素
auto first_five = std::views::iota(0) | std::views::take(5);
// iota(0) 是无限范围,但 take(5) 限制了迭代次数
// 内部用计数器实现,每 ++ 计数器 +1,达到 N 次后 == sentinel
// drop_view:跳过前 N 个元素
// begin() 直接调用 ranges::next(base_.begin(), count_)
auto skip_five = v | std::views::drop(5);
// 注意:drop_view 的 begin() 是 O(N)!
关键实现细节------drop_view::begin() 的缓存:
cpp
template<typename V>
class drop_view {
V base_;
std::iter_difference_t<iterator_t<V>> count_;
// 缓存 begin 以避免重复 O(N) 计算
mutable std::optional<iterator_t<V>> cached_begin_;
constexpr auto begin() {
if (!cached_begin_) {
cached_begin_ = std::ranges::next(std::ranges::begin(base_), count_);
}
return *cached_begin_;
}
};
4. 管道运算符 | 的魔法
管道运算符是 Ranges 最具表现力的语法特性。其实现依赖两个关键机制:范围适配器闭包对象 和 管道运算符重载。
4.1 范围适配器闭包对象(Range Adaptor Closure Object)
cpp
// 概念定义(C++23 正式引入,C++20 中已有隐式使用)
template<typename T>
concept range_adaptor_closure = std::copy_constructible<T> && requires(T t, auto r) {
{ t(r) } -> std::ranges::range; // 可调用,接受 range 返回 range
};
// views::filter 返回一个闭包对象(不是 view!)
auto even_filter = std::views::filter([](int x) { return x % 2 == 0; });
// even_filter 的类型是 __filter_closure,持有谓词,等待一个 range 参数
// 以下两种写法完全等价:
auto r1 = even_filter(v); // 函数调用
auto r2 = v | even_filter; // 管道语法
4.2 管道运算符的实现
cpp
// 标准库中管道运算符的简化实现
template<std::ranges::range R, std::copy_constructible Adaptor>
requires std::invocable<Adaptor, R>
constexpr auto operator|(R&& r, Adaptor&& adaptor) {
return std::invoke(std::forward<Adaptor>(adaptor), std::forward<R>(r));
}
// 关键:operator| 仅仅是把右边的东西以左边为参数调用!
4.3 闭包组合:views::filter(odd) | views::transform(square) 如何工作
cpp
// 闭包对象之间也可以管道组合
auto composed = std::views::filter([](int x) { return x % 2 != 0; })
| std::views::transform([](int x) { return x * x; });
// composed 本身也是一个闭包对象!
// 这是如何实现的?------ 闭包之间管道运算符的另一个重载
template<std::copy_constructible A, std::copy_constructible B>
requires std::invocable<A, std::ranges::range auto&&> &&
std::invocable<B, std::invoke_result_t<A, std::ranges::range auto&&>>
constexpr auto operator|(A&& a, B&& b) {
// 返回一个新的闭包对象,等价于 b(a(x))
return compose_closure(std::forward<A>(a), std::forward<B>(b));
}
// compose_closure 的实现(简化版)
template<typename A, typename B>
struct compose_closure {
A a;
B b;
template<std::ranges::range R>
constexpr auto operator()(R&& r) const {
return std::invoke(b, std::invoke(a, std::forward<R>(r)));
}
};
4.4 管道链求值流程图
注意:整个管道链在编译期构建完成,没有任何运行时开销。类型虽然复杂,但用 auto 接收完全无感。
5. 延迟求值:惰性链的构建与执行
5.1 惰性求值 vs 及早求值
cpp
std::vector<int> v = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
// 及早求值(Eager)------ 创建 actions 的每个步骤都立即执行
namespace views = std::views;
auto eager_count = std::ranges::count_if(
v | views::filter([](int x) { return x % 2 == 0; })
| views::transform([](int x) { return x * x; })
| views::take(3),
[](int x) { return x > 10; }
);
// 求值时机:count_if 需要遍历时,才触发整个管道链
// 惰性求值的核心:filter / transform / take 都不产生新容器
// 只有最终的消费操作(count_if / for_each / copy)才驱动遍历
5.2 遍历过程的微观视角
cpp
// 考虑:偶数 → 平方 → 取前3个
// v = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
// 遍历过程(以 count_if 驱动为例):
/*
迭代 1: filter++ → 跳过 1(不满足偶数)
迭代 2: filter++ → 停在 2(满足偶数)
transform* → 2*2 = 4
take → 计数1(未达上限),输出 4
检查 4 > 10? → false
迭代 3: filter++ → 跳过 3
迭代 4: filter++ → 停在 4
transform* → 16
take → 计数2,输出 16
检查 16 > 10? → true,count++
迭代 5: filter++ → 跳过 5
迭代 6: filter++ → 停在 6
transform* → 36
take → 计数3(达到上限!),输出 36
检查 36 > 10? → true,count++
take 计数达到 3,停止遍历。
最终 count_if 结果: 2
总计:
filter 比较: 6 次(检查了 1,2,3,4,5,6 是否偶数)
transform 调用: 3 次(只处理了 2,4,6)
take 计数: 3 次
最终谓词: 3 次
→ 没有创建任何临时 vector!
*/
5.3 性能对比:惰性链 vs 传统写法
cpp
#include <iostream>
#include <vector>
#include <algorithm>
#include <ranges>
#include <chrono>
void benchmark() {
constexpr size_t N = 10'000'000;
std::vector<int> v(N);
std::iota(v.begin(), v.end(), 1);
// --- 传统写法:三次中间分配 ---
auto t1 = std::chrono::high_resolution_clock::now();
std::vector<int> temp1, temp2, result;
std::copy_if(v.begin(), v.end(), std::back_inserter(temp1),
[](int x) { return x % 3 == 0; }); // ~333万个元素
std::transform(temp1.begin(), temp1.end(), std::back_inserter(temp2),
[](int x) { return x * 2; });
std::copy_n(temp2.begin(), 1000, std::back_inserter(result));
auto t2 = std::chrono::high_resolution_clock::now();
// --- Ranges 写法:零中间分配 ---
auto t3 = std::chrono::high_resolution_clock::now();
auto rng_result = v
| std::views::filter([](int x) { return x % 3 == 0; })
| std::views::transform([](int x) { return x * 2; })
| std::views::take(1000);
std::vector<int> result2(rng_result.begin(), rng_result.end());
auto t4 = std::chrono::high_resolution_clock::now();
auto traditional_ms = std::chrono::duration_cast<std::chrono::milliseconds>(t2 - t1).count();
auto ranges_ms = std::chrono::duration_cast<std::chrono::milliseconds>(t4 - t3).count();
std::cout << "传统写法: " << traditional_ms << "ms\n";
std::cout << "Ranges: " << ranges_ms << "ms\n";
std::cout << "加速比: " << static_cast<double>(traditional_ms) / ranges_ms << "x\n";
}
// 典型输出(MSVC 2022, /O2):
// 传统写法: 87ms
// Ranges: 3ms
// 加速比: 29.0x
// 原因:Ranges 只遍历了约3000个元素(找到1000个3的倍数只需3000次迭代),
// 而传统写法要先遍历全部1000万找到所有~333万个,再transform它们
|---------|-------------------------|--------------|
| 指标 | 传统 STL | Ranges |
| 内存分配次数 | 3 次(temp1/temp2/result) | 1 次(result2) |
| 遍历的源元素数 | 1000 万 + 333 万 + 1000 | ~3000(惰性终止) |
| 中间数据量 | ~333 万个 int(~13 MB) | 0 |
| 总耗时(近似) | ~87ms | ~3ms |
6. 自定义 View 实战
6.1 实现 stride_view:每隔 N 个取一个元素
cpp
#include <ranges>
#include <iterator>
template<std::ranges::input_range V>
requires std::ranges::view<V>
class stride_view : public std::ranges::view_interface<stride_view<V>> {
public:
stride_view() = default;
stride_view(V base, std::iter_difference_t<std::ranges::iterator_t<V>> step)
: base_(std::move(base)), step_(step)
{
if (step_ <= 0) throw std::invalid_argument("step must be positive");
}
class iterator {
public:
using iterator_category = std::input_iterator_tag;
using value_type = std::ranges::range_value_t<V>;
using difference_type = std::iter_difference_t<std::ranges::iterator_t<V>>;
iterator() = default;
iterator(std::ranges::iterator_t<V> current,
std::ranges::sentinel_t<V> end,
difference_type step)
: current_(std::move(current)), end_(std::move(end)), step_(step) {}
decltype(auto) operator*() const { return *current_; }
iterator& operator++() {
// 前进 step_ 步,但不能越过 end_
for (difference_type i = 0; i < step_ && current_ != end_; ++i)
++current_;
return *this;
}
iterator operator++(int) { auto tmp = *this; ++*this; return tmp; }
bool operator==(std::default_sentinel_t) const { return current_ == end_; }
private:
std::ranges::iterator_t<V> current_;
std::ranges::sentinel_t<V> end_;
difference_type step_{1};
};
auto begin() { return iterator(std::ranges::begin(base_), std::ranges::end(base_), step_); }
auto end() { return std::default_sentinel; }
private:
V base_;
std::iter_difference_t<std::ranges::iterator_t<V>> step_{1};
};
// Range Adaptor 闭包对象(配合管道运算符)
struct stride_fn {
std::iter_difference_t<int> step;
template<std::ranges::input_range R>
constexpr auto operator()(R&& r) const {
return stride_view<std::views::all_t<R>>(
std::views::all(std::forward<R>(r)), step);
}
// 支持闭包组合
template<std::ranges::range R>
friend constexpr auto operator|(R&& r, stride_fn s) {
return s(std::forward<R>(r));
}
};
constexpr auto stride(std::iter_difference_t<int> step) {
return stride_fn{step};
}
// === 使用示例 ===
int main() {
std::vector<int> v = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
// 每隔 3 个取一个:0, 3, 6, 9
auto every_third = v | stride(3);
for (int x : every_third) {
std::cout << x << ' '; // 输出: 0 3 6 9
}
// 管道组合
auto result = v
| std::views::filter([](int x) { return x % 2 == 0; }) // 0, 2, 4, 6, 8, 10
| stride(2) // 0, 4, 8
| std::views::transform([](int x) { return x * x; }); // 0, 16, 64
}
6.2 实现 sliding_view:滑动窗口
cpp
template<std::ranges::forward_range V>
requires std::ranges::view<V>
class sliding_view : public std::ranges::view_interface<sliding_view<V>> {
public:
sliding_view() = default;
sliding_view(V base, size_t window_size)
: base_(std::move(base)), window_size_(window_size) {}
class iterator {
public:
using iterator_category = std::input_iterator_tag;
using value_type = std::vector<std::ranges::range_value_t<V>>;
using difference_type = std::ptrdiff_t;
iterator() = default;
iterator(std::ranges::iterator_t<V> start,
std::ranges::sentinel_t<V> end,
size_t window_size)
: start_(std::move(start)), end_(std::move(end)), window_size_(window_size)
{
update_window();
}
value_type operator*() const { return window_; }
iterator& operator++() {
++start_;
update_window();
return *this;
}
iterator operator++(int) { auto tmp = *this; ++*this; return tmp; }
bool operator==(std::default_sentinel_t) const { return window_.size() < window_size_; }
private:
void update_window() {
window_.clear();
auto it = start_;
for (size_t i = 0; i < window_size_ && it != end_; ++i, ++it)
window_.push_back(*it);
}
std::ranges::iterator_t<V> start_;
std::ranges::sentinel_t<V> end_;
size_t window_size_;
value_type window_;
};
auto begin() { return iterator(std::ranges::begin(base_), std::ranges::end(base_), window_size_); }
auto end() { return std::default_sentinel; }
private:
V base_;
size_t window_size_{1};
};
// 使用示例:计算移动平均
// auto moving_avg = data | sliding(5) | transform(avg);
7. 性能考量与优化策略
7.1 编译时间影响
Ranges 的模板层次深、类型名长,对编译时间有显著影响。
cpp
// 类型长度对比
// filter_view<transform_view<filter_view<vector<int>, ...>, ...>, ...>
// 类型名可长达数千字符
// 优化策略 1:限制管道深度
// 不建议
auto r = v | f1 | f2 | f3 | f4 | f5 | f6 | f7 | f8 | f9 | f10;
// 建议:拆分为 2-3 段
auto r1 = v | f1 | f2 | f3 | f4;
auto r2 = r1 | f5 | f6 | f7;
auto r3 = r2 | f8 | f9 | f10;
// 优化策略 2:对大型项目启用预编译头
// 在 pch.h 中 #include <ranges>
7.2 避免常见陷阱
|-------------------------|-------------------------------|---------------------------------------------|
| 陷阱 | 原因 | 解决方案 |
| filter_view 后调用 .size() | filter 不支持 sized_range | 先 std::ranges::distance() 计数 |
| 对临时容器创建 view(悬垂引用) | 临时对象析构后 view 失效 | 避免 auto v = get_vec() | views::filter(...) |
| take_view 后随机访问 | take 后不满足 random_access_range | 先物化(materialize)到容器 |
| 多次遍历 filter_view | 谓词被多次调用 | 缓存结果到 std::vector |
| 管道链中混用 actions | actions 及早求值,破坏惰性链 | 将 actions 放在管道链最末端 |
7.3 borrowed_range:悬垂引用检测
cpp
// borrowed_range 概念标记"迭代器不依赖范围对象生命周期"的范围
template<typename T>
concept borrowed_range = std::ranges::range<T> &&
(std::is_lvalue_reference_v<T> || std::ranges::enable_borrowed_range<std::remove_cvref_t<T>>);
// 安全:vector 是 lvalue,其迭代器有效
std::vector<int> v = {1, 2, 3};
auto safe = std::ranges::subrange(v.begin(), v.end()); // OK
// 危险:临时对象
auto dangerous = std::ranges::subrange(
std::vector<int>{1, 2, 3}.begin(), // 临时 vector 已析构!
std::vector<int>{1, 2, 3}.end()
); // 编译能过,运行 UB
// C++23 防护:std::ranges::dangling
auto view = std::vector<int>{1, 2, 3} | std::views::filter([](int x) { return x > 0; });
// C++20: 返回 filter_view,持有悬垂引用
// C++23: 返回 std::ranges::dangling,编译期标记无效
8. Ranges 与经典 STL 算法对比速查表
8.1 算法对照
|---------|------------------------------------------------------------------------------|------------------------------------|---------------|
| 操作 | 传统 STL | C++20 Ranges | 关键差异 |
| 全范围排序 | sort(v.begin(), v.end()) | ranges::sort(v) | 无需 begin/end |
| 按成员排序 | sort(v.begin(), v.end(), \[\](auto& a, auto& b){ return a.age < b.age; }) | ranges::sort(v, {}, &Person::age) | Projection 简化 |
| 查找 | find(v.begin(), v.end(), val) | ranges::find(v, val) | 返回 iterator |
| 条件查找 | find_if(v.begin(), v.end(), pred) | ranges::find_if(v, pred) | 无需 begin/end |
| 条件计数 | count_if(v.begin(), v.end(), pred) | ranges::count_if(v, pred) | |
| 转换拷贝 | transform(...) | views::transform(f) | 惰性,零拷贝 |
| 筛选拷贝 | copy_if(...) | views::filter(f) | 惰性,零拷贝 |
| 取前 N 个 | copy_n(...) | views::take(n) | 惰性 |
| 跳过前 N 个 | next(begin, n) | views::drop(n) | 惰性 |
| 去重 | unique(...) | views::unique (C++23) | 惰性 |
| 反转遍历 | reverse_iterator | views::reverse | |
| 获取键/值 | 手动遍历 | views::keys / views::values | map 专用 |
| 枚举 | 手动索引 | views::enumerate (C++23) | 带索引遍历 |
| 合并 | views::concat (C++26) | 惰性级联 | |
| 生成序列 | 手写循环 | views::iota(a, b) | 惰性整数序列 |
| 重复值 | fill(...) | views::repeat(v, n) (C++23) | 惰性 |
8.2 常用 View 速查
|------------------------------------------------|--------|-----------------------|
| View | C++ 版本 | 功能 |
| views::filter | C++20 | 条件筛选 |
| views::transform | C++20 | 元素变换 |
| views::take | C++20 | 取前 N 个 |
| views::drop | C++20 | 跳过前 N 个 |
| views::reverse | C++20 | 反向遍历 |
| views::iota | C++20 | 整数序列 |
| views::all | C++20 | 范围→视图 |
| views::take_while | C++20 | 条件取前 |
| views::drop_while | C++20 | 条件跳过 |
| views::join | C++20 | 展平嵌套 |
| views::split | C++20 | 分隔符分割 |
| views::lazy_split | C++20 | 惰性分割 |
| views::common | C++20 | 统一 begin/end 类型 |
| views::elements | C++20 | 取 tuple/pair 的第 N 个元素 |
| views::keys | C++20 | 取 map 的键 |
| views::values | C++20 | 取 map 的值 |
| views::zip | C++23 | 并行遍历多个范围 |
| views::zip_transform | C++23 | 带变换的 zip |
| views::enumerate | C++23 | 带索引遍历 |
| views::chunk | C++23 | 固定大小分块 |
| views::slide | C++23 | 滑动窗口 |
| views::stride | C++23 | 步长跳跃 |
| views::cartesian_product | C++23 | 笛卡尔积 |
| views::as_rvalue | C++23 | 转为右值引用 |
| views::repeat | C++23 | 重复值生成 |
| views::concat | C++26 | 拼接多个范围 |
| views::cache_latest | C++26 | 缓存最近值 |
| views::to (C++23 <ranges> 的 std::ranges::to) | C++23 | 范围→容器 |
9. 总结
C++20 Ranges 不是对 STL 的简单封装,而是对"范围"抽象的一次范式升级:
- 统一的范围表达:std::ranges::range 概念替代迭代器对,配合 sentinel 实现非对称范围
- 组合优于继承:通过管道运算符和闭包组合,无需中间容器即可构建复杂数据流
- 惰性求值:View 适配器不产生数据副本,遍历由最终消费操作驱动
- 投影简化:projection 消除了大量手写比较器的需求,是 Ranges 最被低估的特性
- 编译期安全:borrowed_range 和 dangling 从类型系统层面防止悬垂引用
使用建议:
- 优先使用 std::ranges:: 命名空间下的算法,它们天然支持投影且更安全
- 管道链深度控制在 5 层以内,避免编译时间和调试困难
- 多次遍历场景先用 std::ranges::to<std::vector>()(C++23)物化
- 避免对临时对象创建 view,用 borrowed_range 概念检查安全性
- 配合 C++20 Concepts 约束泛型代码,让编译器提供更友好的错误信息
从 C++20 到 C++26,Ranges 库仍在快速演化,views::concat、views::cache_latest 等新工具将持续拓展惰性组合的边界。