1. 背景介绍
在 C++17 之后,标准库的 std::ranges 提供了基于范围(range)的视图(view)和算法(algorithm),大大提升了序列操作的表达力和效率。2019 年,提议将 ranges-v3(一个第三方库,C++20 之前的 ranges 实现)合并到标准库的路线图中。如今 C++23 已经把 ranges-v3 的核心功能正式纳入标准库,部分实现细节与 ranges-v3 仍有差异。本文以对比两者的实现细节、语义差异和实际使用场景为主,帮助读者在项目中做出更合适的选择。
2. 基本概念回顾
| 术语 | 含义 |
|---|---|
| View | 一个轻量级、懒惰求值的容器,能在不复制元素的情况下对底层数据进行转换。 |
| Pipe | 类似函数式编程中的管道,用 | 运算符把视图组合成更复杂的表达式。 |
| Iterator | 对元素的访问方式,view 本身并不存储元素,只是提供对原始容器的迭代器。 |
3. ranges-v3 的核心特性
- 可组合的视图:如
filter,transform,take,drop,join等。 - 高性能:所有视图都是 懒惰 的,只有在迭代时才会执行转换。
- 兼容 C++14/17:不需要更新编译器就能使用。
#include <range/v3/all.hpp>
using namespace ranges;
int main() {
std::vector <int> nums{1, 2, 3, 4, 5, 6};
auto even_squares = nums
| views::filter([](int n){ return n % 2 == 0; })
| views::transform([](int n){ return n * n; });
for (int val : even_squares)
std::cout << val << ' ';
}
4. C++23 std::ranges 的实现差异
4.1 命名空间与头文件
- ranges-v3:
#include <range/v3/all.hpp>,使用namespace ranges。 - C++23:`#include `,使用 `namespace std::ranges`。
4.2 视图的构造方式
ranges-v3 通过模板参数 auto 捕获函数对象;C++23 在 std::ranges::views 中采用了 管道(pipe)与 通用视图(generic view)组合方式,并在 23 中引入了 std::ranges::views::all_t 以统一容器与视图。
// ranges-v3
auto squares = nums | views::transform([](int n){ return n * n; });
// std::ranges
auto squares = std::views::transform(nums, [](int n){ return n * n; });
4.3 std::ranges::view 与 ranges::view
ranges::view是概念(concept),view是一个包装类;std::ranges::view也是概念,但其实现细节更紧凑,支持viewable_range(可视化范围)。
4.4 迭代器类别
在 ranges-v3 中,views::filter 的迭代器是 filter_view;在 std::ranges 中,迭代器采用 std::ranges::filter_view,但提供了更细粒度的 sentinel 与 iterator 区分。
4.5 性能对比
- 编译时间:
std::ranges的模板更简洁,编译器优化空间更大。 - 运行时:两者都实现了 lazy 计算,性能相差不大;但在极端情况下,
std::ranges的views::iota与views::zip对内存布局做了更好的优化。
5. 典型使用场景
| 场景 | 适用库 | 说明 |
|---|---|---|
| 需要支持 C++14/17 项目 | ranges-v3 | 由于标准库尚未支持,必须使用第三方库。 |
需要使用 std::ranges::views::zip |
C++23 | 该视图在标准库实现中更轻量。 |
| 需要在编译阶段对代码进行重构(如 refactoring) | ranges-v3 | 由于 ranges-v3 提供了更丰富的工具,例如 views::transform 的通用性更高。 |
需要与现有标准库算法(std::for_each, std::accumulate)配合 |
std::ranges | 通过 std::ranges::subrange 可以直接与算法配合。 |
6. 代码实例:C++23 的 views::zip 与 ranges-v3 的 views::zip 对比
// ranges-v3
#include <range/v3/all.hpp>
using namespace ranges;
int main() {
std::vector <int> a{1,2,3};
std::vector <char> b{'a','b','c','d'};
for (auto&& [x, y] : a | views::zip(b))
std::cout << x << '-' << y << ' ';
}
// std::ranges (C++23)
#include <ranges>
int main() {
std::vector <int> a{1,2,3};
std::vector <char> b{'a','b','c','d'};
for (auto&& [x, y] : std::views::zip(a, b))
std::cout << x << '-' << y << ' ';
}
两段代码功能完全相同,区别在于 std::views::zip 的返回类型在编译期更加准确,且对 std::array、std::span 等新容器具有更好的兼容性。
7. 结论与建议
- 如果项目已经使用 C++23 或更高,建议直接使用
std::ranges,其 API 与ranges-v3十分相似,但更符合标准规范,未来的维护成本更低。 - 如果项目受限于 C++14/17,或者需要利用
ranges-v3在std::ranges之前出现的成熟生态(如views::join,views::common等),ranges-v3仍然是优选。 - 混用:在 C++23 项目中可以同时包含
ranges-v3,但需要注意命名冲突,推荐使用using namespace ranges并显式使用std::ranges::views来区分。 - 性能:两者几乎没有显著差异,主要关注编译时间与可读性。
通过上述对比,开发者可以根据项目的语言版本、已有代码以及对性能的细节要求,选择最合适的 ranges 实现。祝编码愉快!