C++20 Ranges 与惰性求值视图

C++20 Ranges 库将函数式编程的管道操作引入标准库,通过惰性求值的 view 和 adapter 让数据处理代码更具表达力。本文深入 Ranges 的核心概念,对比传统 STL 算法、讲解 filter+transform 管道组合、自定义 view 的实现方法,以及 ranges::to 的容器构造技巧,帮助你写出声明式、高效且可组合的 C++ 数据流代码。

C++ 的 <algorithm> 头文件提供了丰富的通用算法,但长期以来,``链式调用始终是一大痛点。写 filter后接transform意味着创建至少两个中间容器,而且代码读起来是「由内到外」的嵌套结构。C++20 Ranges 库从根本上改变了这一现状:它引入了惰性求值的视图(views)和管道操作符|`,让数据处理代码回归声明式的自然阅读顺序。本文将系统梳理 Ranges 的核心概念,展示其与传统 STL 的对比,并深入自定义 view 的实现方法。

一、Ranges 库 vs 传统 STL 算法

1.1 问题:嵌套的 STL 调用

假设我们要筛选出正偶数,然后平方,最后取前 3 个:

#include <vector>
#include <algorithm>
#include <iostream>
#include <iterator>

// 传统 STL:繁琐、创建多个中间容器、阅读顺序与执行顺序相反
std::vector<int> process_stl(const std::vector<int>& data) {
    // 1. 过滤正偶数
    std::vector<int> evens;
    std::copy_if(data.begin(), data.end(), std::back_inserter(evens),
                 [](int x) { return x > 0 && x % 2 == 0; });
    
    // 2. 平方
    std::vector<int> squared;
    std::transform(evens.begin(), evens.end(), std::back_inserter(squared),
                   [](int x) { return x * x; });
    
    // 3. 取前 3 个
    std::vector<int> result;
    auto end = std::min(squared.begin() + 3, squared.end());
    std::copy(squared.begin(), end, std::back_inserter(result));
    
    return result;
}

这段代码存在三个问题:创建了 evens 和 squared 两个中间容器,代码冗长,阅读顺序与意图相反(从下往上读可能是「取前3个 -> 平方 -> 过滤器」)。

1.2 Ranges 解法:声明式管道

C++20 Ranges 用管道操作符 | 将视图串联,每个视图不立即计算,只在遍历时逐项求值:

#include <ranges>
#include <vector>
#include <iostream>

std::vector<int> process_ranges(const std::vector<int>& data) {
    auto view = data
        | std::views::filter([](int x) { return x > 0 && x % 2 == 0; })
        | std::views::transform([](int x) { return x * x; })
        | std::views::take(3);

    // 从 view 构造目标容器(C++23 ranges::to)
    return std::ranges::to<std::vector<int>>(view);  // C++23
}

// C++20 替代方案(没有 ranges::to)
std::vector<int> process_ranges_cpp20(const std::vector<int>& data) {
    std::vector<int> result;
    for (int v : data
         | std::views::filter([](int x) { return x > 0 && x % 2 == 0; })
         | std::views::transform([](int x) { return x * x; })
         | std::views::take(3)) {
        result.push_back(v);
    }
    return result;
}

管道操作符让代码的阅读顺序与执行顺序完全一致:「数据 -> 过滤 -> 变换 -> 取前3」。每一个 std::views:: adapter 返回的都不是容器,而是一个轻量的视图对象——它仅持有对原始数据的引用和变换逻辑。

1.3 惰性求值的核心优势

惰性求值意味着:如果最终只需要前 3 个结果,那么即使输入有 100 万个元素,过滤器也只会检查足够支撑 3 个输出结果的数据量。

#include <ranges>
#include <iostream>

int main() {
    auto numbers = std::views::iota(1, 1000000);  // 1 到 999,999 的惰性序列
    
    auto result = numbers
        | std::views::filter([](int x) { return x % 100000 == 0; })
        | std::views::take(3);
    
    for (int v : result) {
        std::cout << v << " ";  // 输出 100000 200000 300000
    }
    // filter 只执行了约 300000 次检查,而非近 100万 次!
}

二、核心 View 与 Adapter

2.1 常用视图一览

视图功能示例
views::filter(pred)保留满足谓词的元素data | filter([](int x){ return x>0; })
views::transform(fn)对每个元素应用变换data | transform([](int x){ return x*x; })
views::take(n)只取前 n 个data | take(3)
views::drop(n)跳过前 n 个data | drop(5)
views::take_while(pred)从头取直到谓词不满足data | take_while([](int x){ return x<100; })
views::drop_while(pred)跳过直到谓词不满足data | drop_while([](int x){ return x<0; })
views::reverse反向遍历data | reverse
views::elements<N>取 tuple 的第 N 个元素pairs | elements<0>
views::split(delim)按分隔符拆分范围text | split(' ')
views::join展平嵌套范围nested | join
views::iota(start, end)整数序列(含 start 不含 end)iota(1, 10)
views::iota(start)无限递增序列iota(1) | take(100)
views::zip(r1, r2)交错合并两个范围C++23
views::enumerate带索引的枚举C++23

2.2 filter + transform 管道详解

#include <ranges>
#include <vector>
#include <string>
#include <iostream>

struct Person {
    std::string name;
    int age;
};

int main() {
    std::vector<Person> people = {
        {"alice", 30}, {"bob", 17}, {"charlie", 25}, {"diana", 16}
    };

    auto adult_names = people
        | std::views::filter([](const Person& p) { return p.age >= 18; })
        | std::views::transform([](const Person& p) { return p.name; });
    
    for (const auto& name : adult_names) {
        std::cout << name << " ";  // alice charlie
    }
    
    // 视图可以再次进入管道
    auto upper_names = adult_names
        | std::views::transform([](const std::string& s) { return s.length(); });
    
    for (auto len : upper_names) {
        std::cout << len << " ";  // 5 7
    }
}

注意:views::transform 的 lambda 返回了 std::string( person’s name),但视图本身并不存储这些字符串,它只是记录了 lambda 和原始数据的关系。每次遍历时,lambda 被重新计算。

2.3 惰性求值的陷阱

因为视图持有对原始数据的引用,如果原始数据在视图被遍历前被销毁或修改,行为未定义:

auto make_bad_view() {
    std::vector<int> local = {1, 2, 3};
    return local | std::views::filter([](int x) { return x > 1; });  // 危险!
}

// 返回后 local 已销毁,视图成为悬空引用
// auto v = make_bad_view(); // 未定义行为!

经验法则:永远不要让视图的原始数据退出作用域。这在 https://plumephp.com/cpp-memory-model/ 中讨论的引用生命周期规则一脉相承。

三、ranges::to 与容器构造

C++23 引入了 std::ranges::to,让从视图构造容器变得简洁:

#include <ranges>
#include <vector>
#include <set>
#include <string>

auto numbers = std::views::iota(1, 10)
    | std::views::filter([](int x) { return x % 2 == 0; });

// C++23 直接构造
std::vector<int> vec = std::ranges::to<std::vector<int>>(numbers);
std::set<int> sorted = std::ranges::to<std::set<int>>(numbers);  // 顺便排序去重

在 C++20 中需要手动遍历或使用 std::ranges::copy:

// C++20 fallback
std::vector<int> vec;
std::ranges::copy(numbers, std::back_inserter(vec));

四、范围概念(Range Concepts)

Ranges 库基于 C++20 Concepts 定义了一套层次化的范围概念:

Concept要求对应迭代器
std::ranges::range有 begin/endForward 及以上
std::ranges::sized_range知道元素数量(O(1))-
std::ranges::viewO(1) 拷贝/移动/析构-
std::ranges::input_range只读遍历input_iterator
std::ranges::forward_range可多次遍历forward_iterator
std::ranges::bidirectional_range可倒序遍历bidirectional_iterator
std::ranges::random_access_range随机访问random_access_iterator

这些概念在自定义视图和编写接受任意范围作为参数的泛型函数时至关重要——详见 https://plumephp.com/cpp-templates-generics/ 中对 Concepts 的深入讨论。

五、自定义 Range Adapter

当标准视图不足以满足需求时,可以实现自定义的 range adapter。

5.1 自定义 View:平方视图

#include <ranges>
#include <concepts>

// 自定义 view:对每个元素求平方
template <std::ranges::input_range R>
class SquareView : public std::ranges::view_interface<SquareView<R>> {
    R base_;

    struct Iterator {
        std::ranges::iterator_t<R> current_;
        
        using value_type = std::ranges::range_value_t<R>;
        using difference_type = std::ranges::range_difference_t<R>;
        
        Iterator& operator++() { ++current_; return *this; }
        Iterator operator++(int) { auto t = *this; ++*this; return t; }
        
        value_type operator*() const { 
            auto v = *current_;
            return v * v;
        }
        
        bool operator==(const Iterator& other) const {
            return current_ == other.current_;
        }
    };

public:
    explicit SquareView(R base) : base_(std::move(base)) {}
    
    Iterator begin() { return Iterator{std::ranges::begin(base_)}; }
    Iterator end() { return Iterator{std::ranges::end(base_)}; }
};

// 适配工厂函数
template <std::ranges::input_range R>
SquareView<R> square_view(R&& range) {
    return SquareView<std::views::all_t<R>>(std::views::all(std::forward<R>(range)));
}

// 使用
int main() {
    std::vector<int> nums{1, 2, 3, 4, 5};
    for (int v : square_view(nums)) {
        std::cout << v << " ";  // 1 4 9 16 25
    }
}

5.2 管道适配器(Closure)

为了让自定义视图也能通过 | 管道操作符使用,需要提供一个 range adapter closure:

namespace views {
    inline constexpr auto square = [](std::ranges::input_range auto&& r) {
        return square_view(std::forward<decltype(r)>(r));
    };
}

// 然后可以这样用:
// auto result = nums | views::square | std::views::take(3);

完整的管道适配器实现需要处理 partial application(柯里化),这在实践中通常借助标准库即将推出的 std::ranges::views::adaptor 设施来简化。

六、Ranges 与传统算法的性能对比

得益于惰性求值,Ranges 管道在很多场景下比等价的 STL 链式调用更快:

  • 内存分配:视图不分配中间容器,只有最终 ranges::to 可能需要一次分配;
  • 缓存局部性:数据只遍历一次,每个元素依次经过所有变换后被消费;
  • 短路:take(n) 可以提前终止,避免不必要的计算。

但需要注意:

  • 某些 view(如 filter)使迭代器从 RandomAccess 降级为 Bidirectional,可能增加算法复杂度;
  • 管道过长时,每层函子对象的嵌套调用可能带来内联压力;
  • 调试版本中,模板的层层展开会使编译时间和二进制体积增加。

在性能关键路径上,建议先写 Ranges 管道保证表达清晰,再用微基准测量验证是否满足要求——见 https://plumephp.com/cpp-performance-optimization/ 中关于 Google Benchmark 的使用方法。

相关阅读

  • https://plumephp.com/cpp-modern-17-20-23/ — C++20 Concepts 与 Ranges 库的语言基础
  • https://plumephp.com/cpp-stl-algorithms/ — STL 算法复杂度保证与迭代器分类的详细解析
  • https://plumephp.com/cpp-templates-generics/ — Concepts 约束与泛型编程基础

延伸阅读

  • https://plumephp.com/posts/hpc/ — 高性能计算中的数据处理流水线优化范式
  • https://plumephp.com/posts/ai/ — AI 推理框架中的张量操作与惰性求值机制
  • https://plumephp.com/posts/cs-fundamentals/ — 惰性求值在函数式编程理论中的 Lambda 演算表达

文末完整示例

// 完整可运行示例:C++20 Ranges 管道 + 自定义 View + 与传统 STL 对比
// 编译:g++ -std=c++20 -o ranges_demo ranges_demo.cpp
// 注意:libstdc++ 10+ 或 libc++ 15+ 支持完整 Ranges

#include <ranges>
#include <vector>
#include <iostream>
#include <algorithm>
#include <numeric>

// ====== 自定义平方 View ======
template <std::ranges::input_range R>
class SquareView : public std::ranges::view_interface<SquareView<R>> {
    R base_;

public:
    struct Iterator {
        std::ranges::iterator_t<R> current_;
        using value_type = std::ranges::range_value_t<R>;
        using difference_type = std::ranges::range_difference_t<R>;

        Iterator& operator++() { ++current_; return *this; }
        Iterator operator++(int) { auto t = *this; ++*this; return t; }
        bool operator==(const Iterator& o) const { return current_ == o.current_; }
        bool operator!=(const Iterator& o) const { return !(*this == o); }
        value_type operator*() const { auto v = *current_; return v * v; }
    };

    explicit SquareView(R base) : base_(std::move(base)) {}
    Iterator begin() { return Iterator{std::ranges::begin(base_)}; }
    Iterator end() { return Iterator{std::ranges::end(base_)}; }
};

// 工厂函数
template <std::ranges::input_range R>
SquareView<std::views::all_t<R>> make_square_view(R&& r) {
    return SquareView<std::views::all_t<R>>(std::views::all(std::forward<R>(r)));
}

int main() {
    std::vector<int> nums;
    for (int i = -5; i <= 10; ++i) nums.push_back(i);

    std::cout << "=== 原始数据 ===" << std::endl;
    for (int n : nums) std::cout << n << " ";
    std::cout << std::endl;

    // ====== 传统 STL ======
    std::cout << "\n=== 传统 STL ===" << std::endl;
    std::vector<int> filtered;
    std::copy_if(nums.begin(), nums.end(), std::back_inserter(filtered),
                 [](int x) { return x > 0 && x % 2 == 0; });
    std::vector<int> squared_stl;
    std::transform(filtered.begin(), filtered.end(), std::back_inserter(squared_stl),
                   [](int x) { return x * x; });
    for (int n : squared_stl) std::cout << n << " ";
    std::cout << std::endl;

    // ====== Ranges 管道 ======
    std::cout << "\n=== Ranges Pipeline ===" << std::endl;
    auto pipeline = nums
        | std::views::filter([](int x) { return x > 0 && x % 2 == 0; })
        | std::views::transform([](int x) { return x * x; });
    for (int n : pipeline) std::cout << n << " ";
    std::cout << std::endl;

    // ====== 惰性求值演示:iota + take ======
    std::cout << "\n=== Lazy iota + filter + take ===" << std::endl;
    auto infinite = std::views::iota(1)
        | std::views::filter([](int x) { return x % 3 == 0; })
        | std::views::transform([](int x) { return x * x; })
        | std::views::take(5);
    for (int n : infinite) std::cout << n << " ";  // 9 36 81 144 225
    std::cout << std::endl;

    // ====== 自定义 SquareView ======
    std::cout << "\n=== Custom Square View ===" << std::endl;
    for (int n : make_square_view(nums) | std::views::take(5)) {
        std::cout << n << " ";  // 25 16 9 4 1
    }
    std::cout << std::endl;

    // ====== 管道组合:filter -> transform -> reverse -> take ======
    std::cout << "\n=== Complex Pipeline ===" << std::endl;
    auto complex = nums
        | std::views::filter([](int x) { return x >= 0; })
        | std::views::transform([](int x) { return x * 10; })
        | std::views::reverse
        | std::views::take(4);
    for (int n : complex) std::cout << n << " ";  // 100 90 80 70
    std::cout << std::endl;

    // ====== 范围 for + 结构化绑定(C++17)配合 Ranges ======
    std::vector<std::pair<int, std::string>> pairs = {
        {1, "one"}, {2, "two"}, {3, "three"}, {4, "four"}
    };
    auto names = pairs
        | std::views::filter([](const auto& p) { return p.first > 1; })
        | std::views::transform([](const auto& p) { return p.second; });
    std::cout << "\n=== Structured binding with Ranges ===" << std::endl;
    for (const auto& name : names) {
        std::cout << name << " ";
    }
    std::cout << std::endl;

    return 0;
}

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「cpp」更多文章

  1. C++20 Modules 模块化与构建系统演进
  2. C++ 单元测试框架:Google Test、Catch2、Doctest 与 Mock 技巧
  3. C++ 现代设计模式:CRTP、类型擦除、PIMPL 与策略模式