C++20 引入的范围库(Ranges Library) 是对 STL(标准模板库)的重大扩展和改进。它将迭代器和算法的概念提升到一个更高的抽象水平,使代码更简洁、更安全、更具可组合性。范围库的核心是范围(Range),它代表一个可迭代的元素序列,而不需要显式使用迭代器对(如 begin()end())。范围库强调**视图(Views)**的懒惰计算(Lazy Evaluation),允许通过管道式组合适配器来处理数据,而不立即复制或修改底层容器。

范围库定义在头文件 <ranges> 中,并提供命名空间别名 std::views(等价于 std::ranges::views),用于简化视图适配器的使用。以下是范围库的详细讲解,结构包括:介绍、核心概念、范围概念、视图和适配器、算法、示例、最佳实践以及注意事项。内容基于 C++20 标准,并提及 C++23 的相关扩展。

1. 介绍

范围库的目的是解决传统 STL 迭代器模型的痛点:

  • 传统迭代器的缺点:代码冗长(如手动传递 begin()end())、易出错(迭代器失效、类型不匹配)、不易组合(算法难以链式调用)。
  • 范围库的优势
    • 可组合性:通过视图适配器(如 views::filter | views::transform)构建管道,懒惰执行,提高可读性和效率。
    • 安全性:使用概念(Concepts)在编译时检查类型要求,减少运行时错误。
    • 兼容性:与现有 STL 容器和算法兼容,提供 ranges:: 命名空间下的重载版本,支持直接传入范围而非迭代器。
    • 抽象水平:范围可以是迭代器对、计数序列(如 views::counted)、条件终止序列(如 views::take_while)或无限序列(如 views::iota)。

范围库的核心组件包括:

  • 范围访问函数:如 ranges::beginranges::end 等,用于获取范围的迭代器和哨兵(Sentinel)。
  • 视图:轻量级对象,代表对底层数据的非拥有视图,支持懒惰操作。
  • 适配器:函数对象,用于创建视图,如 views::filter
  • 算法ranges:: 版本的 STL 算法,支持范围输入。
  • 概念:如 ranges::rangeranges::view 等,用于模板约束。

C++23 进一步扩展了范围库,添加了更多视图(如 ranges::repeat_viewranges::zip_view 等)和实用功能(如 ranges::to 用于范围转换)。

2. 核心概念
  • 范围(Range):任何提供 begin()end() 的类型都可以隐式转换为范围。范围是半开区间 [begin, end)
  • 视图(View):一种特殊的范围,满足 ranges::view 概念:常量时间拷贝/移动/赋值,常用于懒惰计算。视图不拥有数据,仅提供访问接口。
  • 哨兵(Sentinel):范围的结束标记,可能与迭代器类型不同(如 views::take_while 的哨兵基于谓词)。
  • 悬垂迭代器处理(Dangling Iterator):引入 ranges::dangling 类型,避免返回悬垂迭代器(如临时范围的 end())。
  • 借用范围(Borrowed Range):满足 ranges::borrowed_range 概念的范围,其迭代器可以安全返回而不悬垂。
  • 大小估算ranges::sized_range 表示范围大小可在常量时间获取;ranges::approximately_sized_range 表示可估算大小。
  • 自定义点对象(CPO):如 ranges::begin,允许用户自定义范围行为。
  • 范围原始类型:如 ranges::iterator_t<R> 获取范围的迭代器类型;ranges::range_value_t<R> 获取元素值类型。
  • 其他实用工具
    • ranges::subrange:将迭代器-哨兵对转换为视图。
    • ranges::elements_of:将范围标记为序列而非单个值。
    • C++23 的 ranges::to:从输入范围构造新非视图对象(如 auto vec = ranges::to<std::vector>(some_view);)。

范围库还引入了辅助模板,如 non-propagating-cache(类似 std::optional 但不传播常量性)和整数类型辅助(如 make-signed-like-t)。

3. 范围概念(Range Concepts)

范围库使用 C++20 概念进行编译时约束,确保模板安全。关键概念包括:

  • ranges::range:基本范围,提供 beginend
  • ranges::view:视图范围,常量时间拷贝/赋值。
  • ranges::input_range / output_range / forward_range / bidirectional_range / random_access_range / contiguous_range:对应迭代器类型(从输入到连续)。
  • ranges::common_range:迭代器和哨兵类型相同。
  • ranges::sized_range:大小常量时间可得。
  • ranges::borrowed_range:迭代器可安全借用。
  • ranges::constant_range:元素只读。
  • ranges::viewable_range:可安全转换为视图。

这些概念替换了预 C++20 的 SFINAE,提高了错误诊断。

4. 视图和适配器(Views and Adaptors)

视图是范围库的核心,提供懒惰视图工厂和适配器。适配器是函数对象(Range Adaptor Objects),可通过管道 | 组合,形成链式表达式。

4.1 范围工厂(Range Factories)(创建独立视图)
  • ranges::empty_view<T>:空视图,无元素。
  • ranges::single_view<T>:包含单个元素的视图。
  • ranges::iota_view:无限序列,从初始值递增(如 views::iota(1) 生成 1,2,3,…)。
  • C++23 的 ranges::repeat_view:重复相同值的无限序列。
  • ranges::basic_istream_view:从输入流读取元素的视图。
4.2 范围适配器(Range Adaptors)(转换现有范围)

适配器在 std::views 中定义,可链式使用。常见包括:

  • views::all:将范围转换为视图(默认适配)。
  • views::ref:引用视图,不拥有数据。
  • C++23 的 views::owning:拥有范围的视图。
  • views::filter:过滤满足谓词的元素(ranges::filter_view)。
  • views::transform:对每个元素应用函数(ranges::transform_view)。
  • views::take / views::take_while:取前 N 个或直到谓词为假。
  • views::drop / views::drop_while:跳过前 N 个或直到谓词为假。
  • views::join / C++23 的 views::join_with:扁平化范围的范围(加入分隔符)。
  • views::split / views::lazy_split:按分隔符分割。
  • views::reverse:反转视图。
  • views::common:转换为公共范围(迭代器和哨兵类型相同)。
  • views::elements<N> / views::keys / views::values:提取元组/对的元素(keys 为第一元素,values 为第二)。
  • C++23 的扩展:
    • views::enumerate:添加索引(如 (index, value))。
    • views::zip / views::zip_transform:并行多个范围,应用变换。
    • views::adjacent / views::adjacent_transform:相邻元素元组。
    • views::chunk / views::chunk_by:分块视图。
    • views::slide:滑动窗口。
    • views::stride:步长跳跃。
    • views::cartesian_product:笛卡尔积。
    • views::as_const / views::as_rvalue:转换为常量或右值视图。

适配器是闭包对象(Range Adaptor Closure Objects),支持柯里化(如 auto f = views::filter(pred);)。

视图基类 ranges::view_interface 使用 CRTP(Curiously Recurring Template Pattern)简化自定义视图。

5. 算法(Algorithms)

范围库提供 std::ranges:: 命名空间下的算法重载,支持直接传入范围,而非迭代器对。这些算法是急切的(Eager),立即执行操作。它们与传统 <algorithm> 中的算法类似,但更安全和通用。

5.1 算法分类
  • 非修改序列操作ranges::findranges::find_ifranges::countranges::mismatchranges::equalranges::for_each 等。
  • 修改序列操作ranges::copyranges::moveranges::transformranges::replaceranges::fillranges::generateranges::removeranges::uniqueranges::reverseranges::rotateranges::shuffleranges::sample 等。
  • 分区和排序ranges::is_partitionedranges::partitionranges::stable_partitionranges::sortranges::stable_sortranges::partial_sortranges::nth_elementranges::is_sorted 等。
  • 二分搜索ranges::lower_boundranges::upper_boundranges::equal_rangeranges::binary_searchranges::is_heap 等。
  • 集合操作ranges::mergeranges::inplace_mergeranges::includesranges::set_unionranges::set_intersectionranges::set_differenceranges::set_symmetric_difference 等。
  • 堆操作ranges::make_heapranges::push_heapranges::pop_heapranges::sort_heap 等。
  • 最小/最大ranges::minranges::maxranges::minmaxranges::clampranges::lexicographical_compare 等。
  • 排列ranges::is_permutationranges::next_permutationranges::prev_permutation 等。
  • 数值算法<numeric>):ranges::iotaranges::accumulateranges::reduceranges::inner_productranges::partial_sumranges::exclusive_scanranges::inclusive_scanranges::transform_reduceranges::transform_exclusive_scanranges::transform_inclusive_scanranges::adjacent_difference 等。
  • 未初始化内存<memory>):ranges::uninitialized_copyranges::uninitialized_fillranges::uninitialized_moveranges::destroyranges::construct_at 等。
5.2 与传统算法的区别
  • 支持范围输入:如 ranges::sort(vec); 而非 std::sort(vec.begin(), vec.end());
  • 投影(Projection):许多算法支持投影函数,如 ranges::sort(vec, {}, &Person::age);(按年龄排序)。
  • 返回值:许多算法返回结构化结果,如 ranges::find 返回 {it, end} 的子范围。
  • 并行执行:支持 std::execution::par 等策略(C++17 扩展)。

C++23 未直接添加新算法,但增强了范围转换(如 ranges::to)以更好地与算法集成。

6. 示例

以下是完整、可编译的示例(需 C++20+,如 g++ -std=c++20)。示例展示视图组合、算法使用和益处。

示例 1: 基本视图管道(过滤和变换)
#include <iostream>
#include <vector>
#include <ranges>

int main() {
    std::vector<int> vec = {1, 2, 3, 4, 5, 6};

    // 视图管道:过滤偶数,加倍,反转
    auto view = vec | std::views::filter([](int x) { return x % 2 == 0; })
                    | std::views::transform([](int x) { return x * 2; })
                    | std::views::reverse;

    // 懒惰计算:仅在迭代时执行
    for (int x : view) {
        std::cout << x << " ";  // 输出: 12 8 4
    }
    std::cout << std::endl;

    return 0;
}

益处:比传统迭代器代码更简洁,无需手动循环。

示例 2: 算法与范围
#include <iostream>
#include <vector>
#include <ranges>
#include <algorithm>  // for ranges::sort 等

int main() {
    std::vector<int> vec = {5, 3, 1, 4, 2};

    // 范围排序
    std::ranges::sort(vec);

    // 输出: 1 2 3 4 5
    for (int x : vec) {
        std::cout << x << " ";
    }
    std::cout << std::endl;

    // 查找子范围
    auto result = std::ranges::find_if(vec, [](int x) { return x > 3; });
    if (result != vec.end()) {
        std::cout << "First >3: " << *result << std::endl;  // 输出: 4
    }

    return 0;
}

益处:直接传入容器,自动处理范围。

示例 3: C++23 扩展(zip 和 enumerate)
#include <iostream>
#include <vector>
#include <ranges>

int main() {
    std::vector<int> a = {1, 2, 3};
    std::vector<char> b = {'a', 'b', 'c'};

    // zip 两个范围
    auto zipped = std::views::zip(a, b);

    for (auto [x, y] : zipped) {
        std::cout << x << y << " ";  // 输出: 1a 2b 3c
    }
    std::cout << std::endl;

    // enumerate 添加索引
    auto enumerated = std::views::enumerate(a);
    for (auto [idx, val] : enumerated) {
        std::cout << idx << ":" << val << " ";  // 输出: 0:1 1:2 2:3
    }
    std::cout << std::endl;

    // ranges::to 转换为容器
    auto vec_from_view = std::ranges::to<std::vector>(zipped | std::views::transform([](auto t) { return std::get<0>(t) + 10; }));
    for (int x : vec_from_view) {
        std::cout << x << " ";  // 输出: 11 12 13
    }
    std::cout << std::endl;

    return 0;
}

注意:需 C++23 支持。

示例 4: 无限范围与 take
#include <iostream>
#include <ranges>

int main() {
    // 无限序列,取前 10 个
    auto infinite = std::views::iota(1) | std::views::take(10);

    for (int x : infinite) {
        std::cout << x << " ";  // 输出: 1 2 3 4 5 6 7 8 9 10
    }
    std::cout << std::endl;

    return 0;
}
7. 最佳实践
  • 优先使用视图管道:链式组合适配器,提高代码可读性(如 vec | views::filter | views::transform)。
  • 懒惰 vs 急切:视图懒惰计算,适合大数据集;算法急切执行,适合小规模修改。
  • 概念约束:在自定义模板中使用范围概念,如 requires ranges::random_access_range<R>
  • 避免悬垂:使用 borrowed_rangedangling 处理临时范围。
  • 性能优化:视图零开销抽象,但链太长可能影响编译时间;使用 views::common 确保兼容传统算法。
  • 与 STL 集成:范围算法可与传统迭代器混合,但优先范围版本。
  • C++23 迁移:利用 ranges::to 简化视图到容器的转换;探索新视图如 zip 用于并行处理。
  • 调试:使用 static_assert 检查概念;避免在视图中修改底层数据(视图常为只读)。
8. 注意事项
  • 兼容性:需 C++20+ 编译器(如 GCC 10+、Clang 10+、MSVC 19.28+)。C++23 功能需更新版本。
  • 性能开销:视图懒惰,但复杂链可能增加运行时开销;测试大数据集。
  • 限制:并非所有 STL 算法都有范围版本;无限范围需小心,避免无限循环。
  • 自定义范围:继承 view_interface 实现自定义视图,确保满足概念。
  • 更新:到 2025 年,C++23 已标准化,C++26 可能添加 views::concat 等,但范围库核心稳定。
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐