在C++20之前,处理容器数据的主要方式是使用迭代器。这种模式虽然灵活,但存在几个明显的痛点:
std::ranges通过引入范围(Range)抽象解决了这些问题。一个范围可以是:
关键理解:范围适配器(views)不是容器,而是对现有范围的惰性转换。这意味着它们几乎不产生运行时开销。
最常见的三种适配器组合:
cpp复制// 过滤+转换经典模式
auto processed = data
| views::filter([](auto x){ return x > 0; }) // 只保留正数
| views::transform([](auto x){ return x*x; }); // 计算平方
// 取前N个元素
auto top5 = data | views::take(5);
// 反转范围
auto reversed = data | views::reverse;
理解这一点至关重要:当写下data | views::filter(...)时,实际上没有任何计算发生。计算只在以下情况触发:
这种设计带来了显著的性能优势:
标准库提供了约20种常用适配器,但有时需要自定义:
cpp复制template <typename Range>
auto to_hex(Range&& r) {
return std::forward<Range>(r)
| views::transform([](int x){
std::stringstream ss;
ss << std::hex << x;
return ss.str();
});
}
使用时:
cpp复制auto hex_values = data | to_hex();
std::ranges定义了一套完整的概念体系:
code复制range
├── input_range
├── forward_range
├── bidirectional_range
├── random_access_range
└── contiguous_range
每个算法会声明其要求的概念,例如:
sort需要random_access_rangeunique需要forward_rangereverse需要bidirectional_range传统方式的问题:
cpp复制std::list<int> lst;
std::sort(lst.begin(), lst.end()); // 运行时崩溃!
ranges方式:
cpp复制std::ranges::sort(lst); // 编译错误:list不满足random_access_range
错误信息会明确指出:
code复制error: static assertion failed: std::ranges::sort requires a random_access_range
扩展系统定义自己的约束:
cpp复制template <typename T>
concept EvenSizedRange =
std::ranges::range<T> &&
(std::ranges::size(T{}) % 2 == 0);
void process_pairs(EvenSizedRange auto&& r) {
// 保证传入的范围大小是偶数
}
| 传统STL | std::ranges | 改进点 |
|---|---|---|
std::sort(begin, end) |
std::ranges::sort(r) |
更简洁 |
std::copy(src_begin, src_end, dest) |
std::ranges::copy(src, dest) |
避免迭代器不匹配 |
std::find(begin, end, value) |
std::ranges::find(r, value) |
统一接口 |
chunk_by - 按条件分组:
cpp复制std::vector nums{1,1,2,2,3,4,5,5};
auto chunks = nums | views::chunk_by([](int x, int y){
return x == y; // 连续相等元素分组
});
for (auto chunk : chunks) {
// chunk是子范围视图
}
slide - 滑动窗口:
cpp复制auto windows = nums | views::slide(3); // 3元素滑动窗口
过早物化:
cpp复制// 错误:不必要的vector创建
auto vec = data | views::filter(pred) | views::transform(fn)
| ranges::to<std::vector>();
// 正确:保持视图直到必须物化
auto view = data | views::filter(pred) | views::transform(fn);
重复计算:
cpp复制// 低效:多次计算相同视图
if (!(data | views::filter(pred)).empty()) {
process(data | views::filter(pred));
}
// 高效:缓存视图
auto filtered = data | views::filter(pred);
if (!filtered.empty()) {
process(filtered);
}
使用views::single替代临时容器:
cpp复制// 传统方式
std::vector<int> tmp{42};
process(tmp);
// ranges方式
process(views::single(42));
优先使用views::join替代嵌套循环:
cpp复制std::vector<std::vector<int>> matrix;
// 传统方式
for (const auto& row : matrix) {
for (int x : row) { /*...*/ }
}
// ranges方式
for (int x : matrix | views::join) { /*...*/ }
处理服务器日志的典型场景:
cpp复制struct LogEntry { /*...*/ };
std::vector<LogEntry> logs;
// 构建处理管道
auto error_counts = logs
| views::filter([](const LogEntry& e){
return e.level == LogLevel::Error;
})
| views::transform([](const LogEntry& e){
return e.service_name;
})
| views::group_by(std::identity{}) // 按服务名分组
| views::transform([](auto&& group){
return std::pair{
*group.begin(),
std::ranges::distance(group)
};
});
游戏开发中的典型应用:
cpp复制std::vector<GameObject> objects;
// 获取所有可见的敌人并按距离排序
auto targets = objects
| views::filter(&GameObject::is_visible)
| views::filter(&GameObject::is_enemy)
| views::transform([player](const GameObject& obj){
return std::pair{obj, distance(player, obj)};
})
| views::sort([](auto&& a, auto&& b){
return a.second < b.second;
});
C++20协程可以与ranges产生强大协同效应:
cpp复制generator<int> fibonacci() {
int a = 0, b = 1;
while (true) {
co_yield a;
std::tie(a, b) = std::pair{b, a + b};
}
}
void process() {
// 取前10个偶数斐波那契数
auto nums = fibonacci()
| views::filter([](int x){ return x % 2 == 0; })
| views::take(10);
for (int n : nums) {
// 处理...
}
}
这种组合允许:
从最简单的算法开始替换:
cpp复制// 之前
std::sort(data.begin(), data.end());
// 之后
std::ranges::sort(data);
将复杂循环拆分为视图管道:
cpp复制// 之前
std::vector<Result> output;
for (const auto& item : input) {
if (item.valid()) {
output.push_back(process(item));
}
}
// 之后
auto output = input
| views::filter(&Item::valid)
| views::transform(process)
| ranges::to<std::vector>();
当需要与传统API交互时:
cpp复制void legacy_api(const int* begin, const int* end);
void wrapper(std::ranges::contiguous_range auto&& r) {
// 将范围转换为传统迭代器对
legacy_api(std::ranges::data(r), std::ranges::data(r) + std::ranges::size(r));
}
由于视图是惰性的,传统调试方法可能不直观。推荐:
使用ranges::to_vector即时查看内容:
cpp复制auto debug_view = some_complex_view | ranges::to<std::vector>();
使用自定义debug视图:
cpp复制auto debug = views::transform([](auto x){
std::cout << "Processing: " << x << "\n";
return x;
});
auto result = data | debug | views::filter(/*...*/);
测量范围管道性能时注意:
基准测试示例:
cpp复制auto view = data | views::filter(pred) | views::transform(fn);
// 只测量迭代性能
auto start = std::chrono::high_resolution_clock::now();
for (auto x : view) { /*...*/ }
auto end = std::chrono::high_resolution_clock::now();
虽然std::ranges已经非常强大,但仍有改进空间:
在实际项目中采用std::ranges的经验表明,它确实显著提高了代码的可读性和安全性。一个中型代码库的统计显示: