C++20 Ranges 库实现原理深度解析:从视图适配器到管道运算符

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 的简单封装,而是对"范围"抽象的一次范式升级:

  1. 统一的范围表达:std::ranges::range 概念替代迭代器对,配合 sentinel 实现非对称范围
  2. 组合优于继承:通过管道运算符和闭包组合,无需中间容器即可构建复杂数据流
  3. 惰性求值:View 适配器不产生数据副本,遍历由最终消费操作驱动
  4. 投影简化:projection 消除了大量手写比较器的需求,是 Ranges 最被低估的特性
  5. 编译期安全: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 等新工具将持续拓展惰性组合的边界。

相关推荐
Billy121384 天前
C++20 Concepts:约束与概念的现代范式
c++20·c++进阶学习
可爱系程序猿4 天前
mfc90.dll 缺失如何处理?VC++ 2008 运行库与并行程序集排查记录
程序人生·电脑·c++20
code_pgf5 天前
C++11 / C++14 / C++17 / C++20 新特性总结
c++·c++20
C语言Plus6 天前
C++ SqlBuilder一个简单、灵活且类型安全的 C++ SQL 构建器库
数据库·sql·安全·c++20
CHANG_THE_WORLD16 天前
C++20 协程从零入门:用 ASCII 图彻底理解 `co_await`、协程帧与异步执行
c++20
无限的鲜花1 个月前
协程本质是函数加状态机——零基础深入浅出 C++20 协程
c++·算法·c++20
十五年专注C++开发2 个月前
全面深入了解C++20 范围库(std::ranges)
c++20·管道·哨兵·视图·ranges
小小龙学IT2 个月前
C++20 协程深度解析:从原理到高性能异步框架实战
junit·c++20
楼田莉子2 个月前
C++20新特性:协程
开发语言·c++·后端·学习·c++20