跳至内容

C++20 Ranges:全新范式

探索 C++20 Ranges──STL 算法接口的现代重新设计。学习范围概念、基于范围的算法、投影以及用于可组合数据变换的管道运算符。

为什么需要 Ranges?

C++20 Ranges 是对 STL 算法模型的根本性升级。传统 STL 要求向每个算法传递迭代器对,这既冗长又容易出错(你可能意外传递来自不同容器的迭代器)。Ranges 通过允许你直接传递容器本身──或任何定义了 begin() 和 end() 的对象来解决这个问题。

相比传统算法的关键改进:

1. 直接传递容器 ── std::ranges::sort(vec) 而不是 std::sort(vec.begin(), vec.end())
2. 投影 ── 在比较之前变换元素,而不修改它们
3. 概念 ── 算法有清晰的编译时要求
4. 可组合性 ── 使用管道运算符 | 链接操作
5. 惰性求值 ── 视图按需计算元素,避免中间副本

范围概念

C++20 在 中定义了一个范围概念层次结构:

- std::ranges::range ── 具有 begin() 和 end()
- std::ranges::sized_range ── 具有 O(1) 的 size()
- std::ranges::input_range ── 可以读取一次(类似输入迭代器)
- std::ranges::forward_range ── 多遍正向遍历
- std::ranges::bidirectional_range ── 正向 + 反向
- std::ranges::random_access_range ── 双向 + O(1) 跳跃
- std::ranges::contiguous_range ── 随机访问 + 连续内存
- std::ranges::common_range ── begin() 和 end() 返回相同类型
- std::ranges::viewable_range ── 可以转换为视图

这些概念用于约束基于范围的算法,当你传递不兼容的范围时会给出清晰的错误信息。

基于范围的算法

基于范围的算法位于 std::ranges:: 中,接受整个容器。它们还返回更丰富的结果类型,而不是裸迭代器。

range_algorithms.cpp
#include <algorithm>
#include <ranges>
#include <vector>
#include <string>
#include <iostream>

struct Employee {
    std::string name;
    int age;
    double salary;
};

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

    // Pass the container directly — no begin/end needed
    std::ranges::sort(nums);
    // nums: {1, 1, 2, 3, 4, 5, 5, 6, 9}

    // find returns a borrowed iterator (safe with temporary ranges)
    auto it = std::ranges::find(nums, 4);
    if (it != nums.end()) {
        std::cout << "Found: " << *it << '\n';
    }

    // count_if with ranges
    auto even_count = std::ranges::count_if(nums, [](int x) { return x % 2 == 0; });
    std::cout << "Even count: " << even_count << '\n';  // 3

    // min/max on entire range
    auto [min_it, max_it] = std::ranges::minmax_element(nums);
    std::cout << "Min: " << *min_it << ", Max: " << *max_it << '\n';

    // Sorting employees by salary (descending)
    std::vector<Employee> team = {
        {"Alice", 30, 95000},
        {"Bob", 25, 72000},
        {"Charlie", 35, 110000},
    };
    std::ranges::sort(team, std::ranges::greater{}, &Employee::salary);
    // Charlie (110k), Alice (95k), Bob (72k)

    for (const auto& e : team) {
        std::cout << e.name << ": $" << e.salary << '\n';
    }
}

投影──比较前变换

投影是基于范围算法的独特功能。它们让你指定如何「查看」每个元素以进行比较或处理,而不修改元素本身。

projections.cpp
#include <algorithm>
#include <ranges>
#include <vector>
#include <string>
#include <iostream>
#include <cctype>

int main() {
    // Sort strings case-insensitively using a projection
    std::vector<std::string> words = {"Banana", "apple", "Cherry", "date"};

    std::ranges::sort(words, {}, [](const std::string& s) {
        std::string lower = s;
        std::transform(lower.begin(), lower.end(), lower.begin(),
                       [](char c) { return std::tolower(c); });
        return lower;
    });
    // words: {"apple", "Banana", "Cherry", "date"}
    // Original strings preserved, sorted by lowercase version

    for (const auto& w : words) std::cout << w << ' ';
    std::cout << '\n';

    // Using member pointer as projection
    struct Point { int x, y; };
    std::vector<Point> points = {{3, 1}, {1, 5}, {2, 3}};

    // Sort by x coordinate
    std::ranges::sort(points, {}, &Point::x);
    // points: {{1,5}, {2,3}, {3,1}}

    // Find max by y coordinate
    auto max_y = std::ranges::max_element(points, {}, &Point::y);
    std::cout << "Max y: (" << max_y->x << ", " << max_y->y << ")\n";  // (1, 5)

    // Binary search with projection
    bool found = std::ranges::binary_search(points, 2, {}, &Point::x);
    std::cout << std::boolalpha << "Has x=2: " << found << '\n';  // true
}

管道运算符的组合

管道运算符 | 是 C++20 Ranges 的标志。它让你将范围适配器(视图)链接成一个从左到右阅读的管道,类似于 Unix shell 管道:

auto result = numbers
    | std::views::filter([](int x) { return x % 2 == 0; })
    | std::views::transform([](int x) { return x * x; })
    | std::views::take(5);

这读起来很自然:「取 numbers,保留偶数,将它们平方,取前 5 个。」该管道是惰性的──在你迭代 result 之前不会发生任何计算。这意味着不会分配中间 vector。

我们将在下一课深入探讨视图。

常见陷阱

范围算法可以防止来自临时范围的悬空迭代器。如果你传递一个临时容器,返回的迭代器会悬空,因为临时对象在分号处被销毁:

auto it = std::ranges::find(get_vector(), 42);  
// Returns std::ranges::dangling instead of an iterator!
// Attempting to dereference it won't compile.

这是一个编译时安全特性──传统的 std::find 会静默地返回悬空迭代器,导致运行时的未定义行为。std::ranges::dangling 哨兵类型使其成为编译错误。

最佳实践

在新代码中优先使用 std::ranges:: 算法──它们更安全(无迭代器不匹配、悬空保护)、更可读(直接传容器)、更强大(投影、概念)。

在以下情况使用传统 std:: 算法:
- 你的代码库需要支持 C++20 之前的编译器
- 你需要 ranges 未提供的特定重载
- 你使用来自遗留 API 的迭代器对

注意:std::ranges:: 算法不是直接替换。某些算法返回不同的类型(例如,std::ranges::sort 返回排序范围的尾迭代器)。迁移时请查阅文档。

关键要点
  • C++20 Ranges 允许你直接将容器传给算法──不再需要 begin()/end() 样板代码
  • 投影在比较之前变换元素,而不修改原始数据
  • 范围概念(input_range、random_access_range 等)提供清晰的编译时约束
  • 管道运算符 | 实现可组合的、从左到右的数据处理管道
  • std::ranges::dangling 在编译时防止悬空迭代器 bug
  • 在新的 C++20 代码中优先使用 std::ranges:: 算法,以获得更好的安全性、可读性和功能

测验 — 检验你的知识

(20 经验值)

1. C++20 范围算法中的投影是什么?

2. 用临时容器调用 `std::ranges::find` 会返回什么?

3. `std::ranges::sort(vec, std::ranges::greater{}, &Employee::salary)` 做了什么?