掌握 C++20 中的 Ranges:从“for”到“管道”

C++20 引入的 ranges 库为容器操作提供了更为直观、表达式化的语法。相比传统的 for 循环、迭代器组合,ranges 让我们能以更接近“管道”的方式编写代码,既易读又能让编译器在编译期完成更多优化。本文将从基础使用、常见视图(view)以及自定义视图的实现几个方面,详细探讨如何在实际项目中高效运用 C++20 ranges。

1. Ranges 与传统迭代器的区别

传统写法:

std::vector <int> v = {1,2,3,4,5};
for(auto it = v.begin(); it != v.end(); ++it){
    std::cout << *it << ' ';
}

在这个循环里,我们要显式地处理迭代器的起点和终点,甚至要在每一步手动解引用。若想做过滤、映射等操作,需要配合 std::transformstd::copy_if 等算法,代码显得冗长。

而使用 ranges 则可以这样写:

#include <ranges>
#include <iostream>

std::vector <int> v = {1,2,3,4,5};
for(auto x : v | std::views::filter([](int n){ return n%2==0; })) {
    std::cout << x << ' ';
}

这里我们使用了 | 管道符,把容器与一个 filter 视图连接起来。filter 视图会在迭代时动态判断元素是否满足条件。整个过程无需显式管理迭代器,语义更为直观。

2. 常用视图(views)快速上手

视图 作用 示例
std::views::all 获取容器的默认视图 `auto v = std::vector
{1,2,3}; auto view = v std::views::all;`
std::views::filter 按条件筛选 v | std::views::filter([](int n){ return n>2; })
std::views::transform 对每个元素做映射 v | std::views::transform([](int n){ return n*n; })
std::views::take 截取前 N 个 v | std::views::take(3)
std::views::drop 跳过前 N 个 v | std::views::drop(2)
std::views::reverse 反转 v | std::views::reverse
std::views::zip 组合两个容器 auto zipped = std::views::zip(v, u);
std::views::concat 合并容器 auto merged = std::views::concat(v, u);

这些视图都是轻量级的惰性操作,它们不会立即产生新的容器,而是延迟执行,直到真正需要访问元素时才触发。

3. 将 ranges 与算法结合

ranges 允许我们使用 std::ranges:: 命名空间下的算法。与传统算法不同,新的算法可以直接接受视图作为参数,返回一个视图(或值),而不是像 std::sort 需要修改原容器。示例:

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

std::vector <int> data = {5,3,8,1,4};

auto sorted = data | std::views::sort(); // 视图返回已排序的视图

for(int x : sorted)
    std::cout << x << ' '; // 输出 1 3 4 5 8

在这个例子中,std::views::sort() 并没有修改原始 data,而是返回了一个已经排好序的视图。若想要真正复制排序结果,可结合 std::ranges::to<std::vector>()(C++23 的功能):

auto sorted_vec = data | std::views::sort() | std::ranges::to<std::vector>();

4. 自定义视图(Custom View)

有时我们需要一种内置视图未提供的特殊行为。C++20 通过 std::ranges::view_interface 让实现自定义视图变得简单。下面演示一个自定义视图 my_transform_view,它对输入容器进行平方操作。

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

template<std::ranges::input_range R>
requires std::is_arithmetic_v<std::ranges::range_value_t<R>>
class my_transform_view : public std::ranges::view_interface<my_transform_view<R>> {
    R base_;
public:
    explicit my_transform_view(R base) : base_(std::move(base)) {}

    auto begin() {
        return std::ranges::begin(base_);
    }

    auto end() {
        return std::ranges::end(base_);
    }

    template<class It>
    class iterator {
        It current_;
    public:
        using iterator_category = std::input_iterator_tag;
        using value_type = decltype((*current_)*(*current_));
        iterator(It current) : current_(current) {}
        value_type operator*() const { return (*current_)*(*current_); }
        iterator& operator++() { ++current_; return *this; }
        bool operator==(const iterator& other) const { return current_ == other.current_; }
    };

    iterator<std::ranges::iterator_t<R>> begin() { return iterator{begin(base_)}; }
    iterator<std::ranges::iterator_t<R>> end()   { return iterator{end(base_)}; }
};

template<std::ranges::input_range R>
my_transform_view(R) -> my_transform_view <R>;

int main() {
    std::vector <int> nums = {1,2,3,4};
    for(int val : nums | my_transform_view{}) {
        std::cout << val << ' ';
    }
}

上述代码中,my_transform_view 对每个元素做平方,并通过继承 std::ranges::view_interface 自动获得了范围(range)接口。调用时只需 nums | my_transform_view{} 即可。

5. 性能与编译期优化

因为 ranges 采用惰性求值,链式视图的所有操作在内部会被聚合为一次遍历,避免了中间容器的产生。编译器还能在编译期推导并消除不必要的临时对象,特别是当视图与 std::ranges::to 结合使用时,编译器可以生成更高效的循环。实际测量表明,使用 ranges 的代码在性能上与手写优化后的循环相当,甚至更优。

6. 代码示例:从文件读取整数并计算平方和

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

int main() {
    std::ifstream fin("numbers.txt");
    std::vector <int> nums{std::istream_iterator<int>(fin), std::istream_iterator<int>()};

    // 只取偶数,平方后求和
    auto result = std::ranges::fold_left(
        nums | std::views::filter([](int n){ return n % 2 == 0; })
             | std::views::transform([](int n){ return n * n; }),
        0LL, std::plus{}
    );

    std::cout << "偶数平方和 = " << result << '\n';
}

上述程序完整演示了文件读取、过滤、映射、聚合的完整管道,全部使用 ranges 表达式,代码简洁且易于维护。

7. 结语

C++20 的 ranges 为容器操作提供了更优雅、声明式的写法。它让代码更像“流水线”,读者可以在单行内完成复杂的容器变换,且编译器能在编译期完成大量优化。掌握 ranges 的基本视图和算法,再结合自定义视图,便能在日常项目中大幅提升代码质量和开发效率。未来,随着标准继续演进,ranges 的功能会愈发丰富,值得每位 C++ 开发者持续关注。

发表评论