C++20 中的 ranges 与视图:简化算法编程

在 C++20 之前,处理容器和算法往往需要一系列繁琐的循环、拷贝和手动管理迭代器。随着标准的演进,ranges 与视图(views)被引入,使得对序列的操作更加直观、内存友好且类型安全。本文将以几个实际场景为例,演示如何使用 ranges 与视图来重构传统的 C++ 代码。

1. 什么是 ranges 与视图?

ranges 是对容器、迭代器和算法的抽象,核心概念包括:

  • Range:可供迭代的序列对象(如 std::vector、std::array 等);
  • View:对 range 的“视图”,可以是过滤、映射、切片等操作,视图本身也是一个 range;
  • Algorithm:对 range 进行操作的函数(如 std::ranges::for_each、std::ranges::sort 等)。

View 的延迟求值特性意味着它们在第一次使用时才真正产生元素,避免了不必要的拷贝。

2. 过滤(filter)

假设我们有一个整数列表,需要找出所有偶数并求和:

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

int main() {
    std::vector <int> data{1,2,3,4,5,6,7,8,9,10};

    auto even_sum = std::accumulate(
        std::ranges::views::filter(data, [](int x){ return x % 2 == 0; })
            .begin(),
        std::ranges::views::filter(data, [](int x){ return x % 2 == 0; })
            .end(),
        0);

    std::cout << "偶数之和: " << even_sum << '\n';
}

上述代码虽然已经比传统循环简洁,但仍需要两次 filter 调用。可以利用 views::transformstd::ranges::views::filter 组合,或者直接使用 std::ranges::accumulate(C++23 提供)来进一步简化。

3. 映射(transform)

在将一个字符串数组转为大写后输出时:

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

int main() {
    std::vector<std::string> words{"hello", "world", "cpp20", "ranges"};

    for (auto word : words | std::ranges::views::transform([](auto&& s){
           std::string res;
           std::transform(std::begin(s), std::end(s), std::back_inserter(res),
                          [](char c){ return std::toupper(static_cast<unsigned char>(c)); });
           return res;
       })) {
        std::cout << word << ' ';
    }
    std::cout << '\n';
}

通过 | 管道符,代码流动性更强,易于阅读。

4. 切片(slice)

要取出向量的中间 5 个元素:

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

int main() {
    std::vector <int> data{10,20,30,40,50,60,70,80,90,100};

    auto middle = data | std::ranges::views::drop(2) | std::ranges::views::take(5);

    for (int x : middle) std::cout << x << ' ';
    std::cout << '\n';
}

这里 drop(2) 跳过前两个元素,take(5) 只取后续 5 个。

5. 组合视图

更复杂的业务逻辑往往需要多个视图组合。下面的例子演示了如何在一行代码中完成:取出长度大于 3 的单词,转为大写,最后收集到新向量中。

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

int main() {
    std::vector<std::string> words{"a", "abc", "abcd", "abcde", "abcde"};

    auto processed = words | std::ranges::views::filter([](auto&& s){ return s.size() > 3; })
                            | std::ranges::views::transform([](auto&& s){
                                std::string res;
                                std::transform(std::begin(s), std::end(s), std::back_inserter(res),
                                               [](char c){ return std::toupper(static_cast<unsigned char>(c)); });
                                return res;
                            });

    std::vector<std::string> result(std::begin(processed), std::end(processed));

    for (auto& w : result) std::cout << w << ' ';
    std::cout << '\n';
}

6. 性能与内存优势

由于视图是延迟求值的,实际上并不会在每一步创建临时容器。比如在上面的例子中,filtertransform 的结果不会单独存储,而是按需产生。与传统一次性拷贝的算法相比,减少了内存占用并提升了缓存友好性。

7. 与传统算法的对比

传统写法:

std::vector <int> data = {1,2,3,4,5,6,7,8,9,10};
std::vector <int> evens;
for (int x : data) {
    if (x % 2 == 0) evens.push_back(x);
}
int sum = 0;
for (int x : evens) sum += x;

使用 ranges:

int sum = std::accumulate(
    std::ranges::views::filter(data, [](int x){ return x % 2 == 0; })
        .begin(),
    std::ranges::views::filter(data, [](int x){ return x % 2 == 0; })
        .end(),
    0);

代码更短、表达更清晰。对于更复杂的链式操作,ranges 甚至可以写成一行。

8. 小结

  • ranges 为容器提供了统一、类型安全的接口;
  • views 通过延迟求值实现了高效的链式操作;
  • 通过 | 管道符,可以像 Unix Shell 一样组合操作,提升代码可读性;
  • 在大多数情况下,ranges 能显著减少临时对象,提升性能。

从 C++20 开始,建议将已有代码逐步迁移到 ranges 语义。随着 C++23 的 std::ranges::accumulate 等新工具加入,写法将更加简洁、直观。祝你在 C++ 之旅中愉快地使用 ranges 与视图,编写更高质量、更高性能的代码!

发表评论