Day10-C++20 Ranges(上):惰性求值与组合式数据处理

C++20 Ranges(上):惰性求值与组合式数据处理

C++进阶计划 · Day 10 | 预计学习时长:2小时

引言

为什么这个知识点重要

如果说 Day 9 的 Concepts 是 C++20 的"宪法",那么 Ranges 就是基于这部宪法构建的"城市"。Concepts 定义了什么是合格的"公民"(类型),而 Ranges 则利用这些概念构建了一套全新的数据处理范式------惰性求值(Lazy Evaluation)组合式编程(Composition)

在传统 C++ 中,处理数据需要这样写:

cpp 复制代码
// 传统方式:每一步都立即计算,临时容器遍布
std::vector<int> nums = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
std::vector<int> evens, squares, results;

std::copy_if(nums.begin(), nums.end(), std::back_inserter(evens),
    [](int n) { return n % 2 == 0; });

std::transform(evens.begin(), evens.end(), std::back_inserter(squares),
    [](int n) { return n * n; });

std::transform(squares.begin(), squares.end(), std::back_inserter(results),
    [](int n) { return n + 10; });

Ranges 让我们可以用数学般的优雅表达:

cpp 复制代码
// Ranges 方式:管道式组合,惰性求值
auto results = nums 
    | std::views::filter([](int n) { return n % 2 == 0; })
    | std::views::transform([](int n) { return n * n; })
    | std::views::transform([](int n) { return n + 10; });
// 这里 transform 调用了两次是合法的,但可以合并为一次:
auto results2 = nums 
    | std::views::filter([](int n) { return n % 2 == 0; })
    | std::views::transform([](int n) { return n * n + 10; });

与 Day 9 的关联

Day 9 的 Concepts 为 Ranges 提供了概念基础。Ranges 库大量使用 Concepts 来定义其核心抽象:

Ranges 核心概念 基于 Day 9 的 Concepts
std::ranges::range begin()end() 的类型
std::ranges::view 不拷贝、不持有数据的轻量范围
std::ranges::input_range 可单遍遍历
std::ranges::viewable_range 可转换为 view 的 range

关键洞察:Concepts 是 C++20 的"类型系统革命",Ranges 则是这场革命在算法和数据结构领域的第一个重大应用。理解 Day 9 的 Concepts,将使你更深入理解 Ranges 的设计哲学。

与 Qt 实战场景的关联

对于 7 年 Qt 开发者,Ranges 在以下场景特别有用:

  • Qt Model/View 数据过滤 :用 filter_view 替代手动过滤 QSortFilterProxyModel
  • 信号槽数据处理 :对 QList<int> 等容器进行函数式处理
  • QML 集成:在 C++ 层预处理数据,直接传递给 QML 列表视图

核心概念

1. Range/View 模型

1.1 什么是 Range

Range 是一个简化的抽象:任何可以用 begin()end() 获取迭代器对的对象。

cpp 复制代码
// Range 的本质:一个有 begin/end 的对象
template <typename T>
concept range = requires(T& t) {
    std::ranges::begin(t);  // 可获取开始迭代器
    std::ranges::end(t);    // 可获取结束迭代器
};

// 所有标准容器都是 Range
static_assert(std::ranges::range<std::vector<int>>);      // ✓
static_assert(std::ranges::range<std::array<int, 10>>);   // ✓
static_assert(std::ranges::range<std::string>);           // ✓
static_assert(std::ranges::range<int[10]>);               // ✓ 数组也是 Range!
1.2 什么是 View

View 是特殊的 Range,它具有两个关键特性:

  1. 不拥有数据:View 通常包装另一个 Range,不自行分配内存
  2. O(1) 拷贝/赋值:拷贝 View 非常廉价,因为数据不在 View 中
cpp 复制代码
// View 的本质:不拥有数据、O(1) 拷贝
// C++20 标准中 view concept 的完整定义(简化)
template <typename T>
concept view = 
    std::ranges::range<T> &&
    std::movable<T> &&
    std::ranges::enable_view<T>;  // 注意:enable_view 是一个变量模板
    // 默认情况下,std::ranges::enable_view<T> 对于满足某些条件的类型为 true

// 更准确地说,标准中的定义还包括:
// - view 必须提供 begin()/end() 且复杂度为 O(1)
// - view 必须可拷贝或可移动(取决于具体类型)
// 
// 实际 enable_view 的定义:
template <typename T>
inline constexpr bool enable_view = 
    std::derived_from<T, view_base> || 
    (std::is_default_constructible_v<T> && std::is_move_constructible_v<T>);
// 对于 span、string_view 等类型,enable_view 被显式特化为 true
// 验证 view 的 O(1) 特性
static_assert(std::ranges::view<std::views::all_t<std::vector<int>&>>);

// 数组视图(不拥有数据)
static_assert(sizeof(std::span<int>) < sizeof(std::vector<int>));  // span 比 vector 小得多

Qt 关联 :可以类比为 Qt 的 QPointerQWeakPointer------它们指向数据,但不拥有数据。

1.3 Range vs View 的关系
复制代码
┌─────────────────────────────────────────────────────────────┐
│                        Range (范围)                          │
│   任何有 begin/end 的类型                                    │
│   - std::vector, std::list, std::array                      │
│   - 普通数组 int[]                                          │
│   - 自定义容器                                              │
├─────────────────────────────────────────────────────────────┤
│                        View (视图)                          │
│   Range 的特殊子集,不拥有数据                                │
│   - std::span (数组视图)                                     │
│   - std::ranges::filter_view                                │
│   - std::ranges::transform_view                             │
│   - std::ranges::drop_view                                  │
└─────────────────────────────────────────────────────────────┘

2. 管道操作符 |

2.1 操作符重载机制

C++20 引入了 | 作为管道操作符,专门用于连接 View。这不是普通的重载,而是 Ranges 库的专门设计:

cpp 复制代码
// 管道操作符的本质:view | adaptor
// 等价于:adaptor(view)

namespace std::ranges::views {
    // 管道操作符的实际实现更复杂,以下是简化概念版:
// 实际上,operator| 的右侧参数是 view adaptor(函数对象),而非 view 本身

// std::views::filter 是一个函数对象,返回一个 filter_view
// auto evens = data | std::views::filter(pred);
// 等价于:auto evens = std::views::filter(pred)(data);

// 标准库中的实现大致如下(简化):
template <typename Rng, typename Adaptor>
    requires std::ranges::viewable_range<Rng>
constexpr auto operator|(Rng&& rng, Adaptor&& adaptor) {
    // Adaptor 是一个可调用对象,接受 range 并返回 view
    return std::forward<Adaptor>(adaptor)(std::forward<Rng>(rng));
}

// 因此 filter(pred) 返回的 adaptor 会接收 data 并构造 filter_view
}

// 所以下面的写法完全等价:
auto v1 = std::views::filter(pred)(rng);     // 直接调用
auto v2 = rng | std::views::filter(pred);    // 管道操作符
2.2 链式调用

管道操作符支持无限链式组合:

cpp 复制代码
std::vector<int> data = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

// 单管道
auto step1 = data | std::views::filter([](int n) { return n % 2 == 0; });

// 链式管道
auto step2 = data 
    | std::views::filter([](int n) { return n % 2 == 0; })
    | std::views::transform([](int n) { return n * n; });

// 更长链式
auto result = data
    | std::views::filter([](int n) { return n > 3; })
    | std::views::drop(2)                    // 跳过前2个
    | std::views::take(3)                    // 只取3个
    | std::views::transform([](int n) { return n * 10; })
    | std::views::reverse;

// 注意:drop 和 take 的参数必须是 std::ptrdiff_t 类型(或可隐式转换)
// 如果传入负数,行为是未定义的!
// std::views::drop(-1)  // ❌ 未定义行为
// std::views::take(-1)  // ❌ 编译错误(C++23 之前)

3. 惰性求值(Lazy Evaluation)

3.1 惰性 vs 立即求值

这是 Ranges 最重要的特性,也是与传统算法的本质区别:

cpp 复制代码
std::vector<int> data = {1, 2, 3, 4, 5};

// 立即求值:transform_vector 立即遍历并生成结果
std::vector<int> eager = [&]{
    std::vector<int> temp;
    for (int n : data) {
        temp.push_back(n * 2);
    }
    return temp;
}();
// 此时 eager 已经是 {2, 4, 6, 8, 10}

// 惰性求值:filter_view 不会立即执行,直到被遍历
auto lazy = data | std::views::filter([](int n) { 
    std::cout << "checking " << n << "\n";  // 不会立即输出!
    return n % 2 == 0; 
});

// 只有在这里真正遍历时,才会调用 lambda
std::cout << "--- First iteration ---\n";
for (int n : lazy) {
    std::cout << "got " << n << "\n";
}

// 输出将是:
// --- First iteration ---
// checking 1
// checking 2
// got 2
// checking 3
// checking 4
// got 4
// checking 5
3.2 惰性求值的优势
cpp 复制代码
// 场景:无限序列 + 惰性求值
// 这在传统 C++ 中是不可能的!

// 生成 0, 1, 2, 3, ... 无限序列(惰性)
auto naturals = std::views::iota(0);

// 永远不需要预先知道要多少个
auto first_100 = naturals | std::views::take(100);      // 取前100个
auto evens = naturals | std::views::filter([](int n) { 
    return n % 2 == 0; 
});  // 所有偶数(仍然惰性)
auto squares = evens | std::views::transform([](int n) {
    return n * n; 
});

// ⚠️ 注意:并非所有 view 都支持随机访问(operator[])
// 上述 squares 是一个 transform_view,它可能支持随机访问
// 但前提是底层 range(evens)支持随机访问

// 安全的方式:使用迭代器或范围 for 循环
for (int n : squares | std::views::take(10)) {
    std::cout << n << " ";  // 0 4 16 36 64 100 144 196 256 324
}

// 或者使用 std::ranges::advance 和迭代器
auto it = squares.begin();
for (int i = 0; i < 10; ++i, ++it) {
    std::cout << *it << " ";
}
}

Qt 关联 :可以类比为 Qt 的 QML Connections------信号处理函数只有在信号发射时才执行,而不是"预先准备好所有结果"。

4. 常用 Views 详解

4.1 std::views::iota - 无限序列生成器
cpp 复制代码
// iota(start) 生成从 start 开始的无限递增序列
auto naturals = std::views::iota(1);                    // 1, 2, 3, 4, ...
auto countdown = std::views::iota(10, 1);               // 10, 9, 8, ... 1
auto steps = std::views::iota(0, 10);                   // 0, 1, 2, ... 9 (有限)

// 与 take 组合:生成指定数量
auto first_20 = std::views::iota(1) | std::views::take(20);

// 生成斐波那契数列(惰性)------ 正确方式
auto fibs_generator = []() {
    return std::views::generate([a = 0, b = 1]() mutable {
        auto result = a;
        auto next = a + b;
        a = b;
        b = next;
        return result;
    });
};

// 使用
auto fibs = fibs_generator();
for (int n : fibs | std::views::take(10)) {
    std::cout << n << " ";  // 0 1 1 2 3 5 8 13 21 34
}

// ⚠️ 注意:std::views::generate 是 C++23 的特性
// 在 C++20 中,可以使用 iota 配合 transform 实现:
auto fibs_cpp20 = std::views::iota(0)
    | std::views::transform([](int n) {
        // 需要使用递归或预计算,无法用单个 transform 优雅实现
        // 推荐使用 generate(C++23)或自定义 view
    });
4.2 std::views::filter - 条件过滤
cpp 复制代码
std::vector<int> data = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

// 过滤偶数
auto evens = data | std::views::filter([](int n) { 
    return n % 2 == 0; 
});

// 过滤复杂的条件
auto filtered = data | std::views::filter([](int n) {
    return n > 3 && n < 9 && n != 5;
});

// filter_view 是惰性的:每次迭代都调用谓词
// 这意味着如果你修改了 data[0],下次迭代 filter 可能会返回不同结果!
4.3 std::views::transform - 元素变换
cpp 复制代码
std::vector<QString> qtStrings = {"Hello", "World", "Qt"};

// 字符串长度
auto lengths = qtStrings | std::views::transform([](const QString& s) {
    return s.length();
});  // 返回 view,不复制字符串

// 平方计算
std::vector<int> nums = {1, 2, 3, 4, 5};
auto squared = nums | std::views::transform([](int n) {
    return n * n;
});  // {1, 4, 9, 16, 25}

// 类型转换
auto asDouble = nums | std::views::transform([](int n) {
    return static_cast<double>(n);
});

// 多重变换
auto complex = nums 
    | std::views::transform([](int n) { return n * 2; })
    | std::views::transform([](int n) { return n + 1; });
4.4 std::views::take / drop - 数量限制
cpp 复制代码
std::vector<int> data = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

// 取前5个
auto first5 = data | std::views::take(5);      // {1, 2, 3, 4, 5}

// 跳过前3个
auto skip3 = data | std::views::drop(3);       // {4, 5, 6, 7, 8, 9, 10}

// 组合:取中间5个 (skip 2, take 5)
auto middle = data | std::views::drop(2) | std::views::take(5);  // {3, 4, 5, 6, 7}

// 无限序列 + take = 有限序列
auto tenSquares = std::views::iota(1) 
    | std::views::transform([](int n) { return n * n; })
    | std::views::take(10);  // 1, 4, 9, 16, 25, 36, 49, 64, 81, 100
4.5 std::views::reverse - 反向遍历
cpp 复制代码
std::vector<int> data = {1, 2, 3, 4, 5};

// 反向遍历(不复制数据)
for (auto it = data.rbegin(); it != data.rend(); ++it) {
    // ...
}

// 使用 reverse_view
for (int n : data | std::views::reverse) {
    std::cout << n << " ";  // 5 4 3 2 1
}

// 组合:反向后取前3个
auto last3 = data | std::views::reverse | std::views::take(3);  // {5, 4, 3}
4.6 std::views::unique - 去重
cpp 复制代码
std::vector<int> data = {1, 1, 2, 2, 2, 3, 1, 1};

std::vector<int> data = {1, 1, 2, 2, 2, 3, 1, 1};

// 去除相邻重复项(只在相邻元素重复时去重)
auto unique_nums = data | std::views::unique;  // {1, 2, 3, 1}
// 注意:结果中的最后一个 1 是因为它和前一个 3 不重复

// ⚠️ 注意:std::views::unique 只处理相邻重复元素!
// 对于需要全局去重的场景,需要先排序

// 结合排序进行全局去重
std::vector<int> sorted_data = data;
std::ranges::sort(sorted_data);
auto sorted_unique = sorted_data | std::views::unique;  // {1, 2, 3}

// ❌ 错误:下面的写法不会排序,只是 transform 无意义
// auto sorted_unique = data 
//     | std::views::transform([](int n) { return n; })  // 这只是复制,不排序
//     | std::views::unique;  // 结果仍然是 {1, 2, 3, 1}
4.7 std::span - 数组视图
cpp 复制代码
// span 是一个不拥有数据的视图,类似于"指针+长度"
std::vector<int> vec = {1, 2, 3, 4, 5};
std::span<int> sp = vec;  // 视图整个 vector

// 子视图
std::span<int> sub = sp.subspan(1, 3);  // {2, 3, 4}

// 前/后元素
// first<3>() 和 last<2>() 需要编译期常量
auto first3 = sp.first<3>();  // ✅ 3 是编译期常量
auto last2 = sp.last<2>();    // ✅ 2 是编译期常量

// 如果大小是运行时变量,使用 first() 和 last()
int n = 3;
// auto first_n = sp.first<n>();  // ❌ 编译错误:n 不是编译期常量
auto first_n = sp.first(n);        // ✅ 运行时版本
auto last_n = sp.last(n);          // ✅ 运行时版本

// 或者使用 subspan
auto sub = sp.subspan(1, 3);  // 运行时参数,{2, 3, 4}

// Qt 关联:类似于 QListView 或 QAbstractItemModel 的"视图"概念
// span 不拥有数据,只提供访问视角

代码实战

实战 1:Qt Model 数据预处理

cpp 复制代码
#include <ranges>
#include <vector>
#include <iostream>
#include <QString>
#include <QDebug>

// 场景:Qt Model 中有一批用户数据,需要过滤、排序后展示

struct User {
    QString name;
    int age;
    bool active;
};

std::vector<User> g_users = {
    {"Alice", 25, true},
    {"Bob", 17, true},
    {"Charlie", 30, false},
    {"Diana", 22, true},
    {"Eve", 28, true},
    {"Frank", 35, false},
    {"Grace", 19, true},
};

// 过滤活跃用户
auto activeUsers = std::views::filter([](const User& u) {
    return u.active;
});

// 过滤成年人(年龄 >= 18)
auto adults = std::views::filter([](const User& u) {
    return u.age >= 18;
});

// 按年龄排序(用于展示)
// ⚠️ 注意:std::views::transform 不会排序!
// 它只是将每个元素转换为另一种类型,顺序保持不变

// 正确的排序方式:先收集到容器,然后使用 std::ranges::sort
auto processed = g_users
    | activeUsers
    | adults
    | std::ranges::to<std::vector>();  // C++23:收集到容器

// 然后排序
std::ranges::sort(processed, [](const User& a, const User& b) {
    return a.age < b.age;
});

// 最后转换输出
auto output = processed 
    | std::views::transform([](const User& u) {
        return QString("  %1 years - %2")
            .arg(u.age)
            .arg(u.name);
    });

// 如果不想使用 C++23 的 to_vector,可以使用:
std::vector<User> processed_vec;
std::ranges::copy(g_users | activeUsers | adults, std::back_inserter(processed_vec));
std::ranges::sort(processed_vec, /* comparator */);

// Qt 中使用
void displayProcessedUsers() {
    qDebug() << "Active adult users (sorted by age):";
    for (const QString& line : processedUsers) {
        qDebug() << line;
    }
    // 输出:
    //   19 years - "Grace"
    //   22 years - "Diana"
    //   25 years - "Alice"
    //   28 years - "Eve"
}

逐行解读

cpp 复制代码
// 第1步:定义过滤器
auto activeUsers = std::views::filter([](const User& u) {
    return u.active;  // 只保留 active == true 的用户
});
// 此时只是创建了一个惰性视图,不做任何计算

// 第2步:链式组合
auto processedUsers = g_users | activeUsers | adults | sortedByAge;
// | 操作符将前面的 view 传递给下一个 adaptor
// 等价于: adults(sortedByAge(activeUsers(g_users)))
// 但更易读、更符合数据流思维

// 第3步:迭代时真正执行
for (const QString& line : processedUsers) {
    // 此时才会依次执行:
    // 1. 遍历 g_users
    // 2. 对每个元素应用 activeUsers 过滤器
    // 3. 对结果应用 adults 过滤器
    // 4. 对结果打包为 pair
    // 5. 转换为 QString
}

实战 2:实现自定义 View Adaptor

cpp 复制代码
#include <ranges>
#include <vector>
#include <iostream>
#include <algorithm>

// 需求:创建一个 view,将奇数翻倍,偶数保持不变

// Step 1: 定义适配器闭包
// 使用 decltype(auto) 显式保留返回类型
auto double_odds = []<std::ranges::viewable_range R>(R&& r) 
    -> decltype(auto) {
    return r | std::views::transform([](int n) {
        return (n % 2 != 0) ? n * 2 : n;
    });
};

// 或者使用 C++20 的 auto 返回类型推导(也可以,但某些情况下可能失败)
auto double_odds_v2 = []<std::ranges::viewable_range R>(R&& r) {
    return r | std::views::transform([](int n) {
        return (n % 2 != 0) ? n * 2 : n;
    });
};

// 为了确保返回类型是 view,可以使用 std::ranges::viewable_range 约束
// 并且确保输入类型满足 viewable_range

// Step 2: 绑定到 std::views(可选,让语法更统一)
namespace views = std::views;

// 测试
int main() {
    std::vector<int> data = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    
    auto result = data 
        | double_odds
        | views::filter([](int n) { return n > 5; })  // 过滤大于5的
        | views::take(3);  // 只取前3个
    
    std::cout << "Result: ";
    for (int n : result) {
        std::cout << n << " ";  // 输出: 6 10 14
    }
    std::cout << "\n";
    
    // 详细分解执行过程:
    // 原始: {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
    // double_odds: {2, 2, 6, 4, 10, 6, 14, 8, 18, 10}
    // filter(>5): {6, 10, 6, 14, 8, 18, 10}
    // take(3): {6, 10, 6}
    
    return 0;
}

逐行解读

cpp 复制代码
// 自定义 adaptor 的关键是返回一个 view
auto my_adaptor = []<std::ranges::viewable_range R>(R&& r) {
    // R&& 是转发引用,可以接受左值和右值
    // viewable_range 确保输入可以被转换为 view
    
    // 返回值是 transform_view,它也是一个 view
    return r | std::views::transform([](int n) {
        return n * 2;  // 具体变换逻辑
    });
};

// 使用时与其他 views 无缝组合
auto combined = data | my_adaptor | std::views::filter(pred);

实战 3:Ranges 与算法结合

cpp 复制代码
#include <ranges>
#include <algorithm>
#include <vector>
#include <numeric>
#include <iostream>

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

    // 传统方式:需要临时容器
    std::vector<int> temp1, temp2, temp3;
    std::copy_if(data.begin(), data.end(), std::back_inserter(temp1),
        [](int n) { return n > 3; });
    std::transform(temp1.begin(), temp1.end(), std::back_inserter(temp2),
        [](int n) { return n * n; });
    std::sort(temp2.begin(), temp2.end());
    
    auto result1 = std::accumulate(temp2.begin(), temp2.end(), 0);

    // Ranges 方式:一行表达式,惰性求值
    auto result2 = data
        | std::views::filter([](int n) { return n > 3; })
        | std::views::transform([](int n) { return n * n; })
        | std::views::common;  // 转换为支持普通迭代器的视图
        // ⚠️ std::ranges::to_vector 是 C++23 特性(P1206R7)
// 在 C++20 中不可用!

// C++20 兼容方式:
std::vector<int> vec;
std::ranges::copy(
    data 
        | std::views::filter([](int n) { return n > 3; })
        | std::views::transform([](int n) { return n * n; }),
    std::back_inserter(vec)
);

// 或者使用 ranges::to(C++23 通用版本):
// auto vec = data | ranges::to<std::vector<int>>();  // 需要 C++23

    // Ranges + 标准算法
    auto processed = data 
        | std::views::filter([](int n) { return n % 2 == 1; })  // 奇数
        | std::views::transform([](int n) { return n * n; })    // 平方
        | std::views::drop(1);                                  // 跳过第一个
    
    // 使用 std::ranges 算法
    auto min_elem = std::ranges::min(processed);
    auto max_elem = std::ranges::max(processed);
    // ⚠️ 注意:std::ranges::fold_left 是 C++23 的特性!
// 在 C++20 中,可以使用 std::accumulate 或 std::ranges::for_each

// C++20 兼容方式:
auto sum = std::accumulate(processed.begin(), processed.end(), 0);

// 或者使用 std::ranges::for_each(C++20)
int sum = 0;
std::ranges::for_each(processed, [&sum](int n) { sum += n; });

// C++23 方式(需要 <numeric> 头文件)
// #include <numeric>
// auto sum = std::ranges::fold_left(processed, 0, std::plus<>{});
    
    std::cout << "Min: " << min_elem << "\n";
    std::cout << "Max: " << max_elem << "\n";
    std::cout << "Sum: " << sum << "\n";
    
    // 验证结果
    // 原始奇数: 3, 1, 5, 9, 5, 3, 5
    // 平方: 9, 1, 25, 81, 25, 9, 25
    // 跳过第一个: 1, 25, 81, 25, 9, 25
    // Min: 1, Max: 81, Sum: 166

    return 0;
}

关键 API 解读

cpp 复制代码
// std::ranges::min/max - 范围版本的极值查询
auto min_elem = std::ranges::min(view);
auto max_elem = std::ranges::max(view);

// std::ranges::fold_left - C++23 的折叠(C++20 可用 accumulate)
auto sum = std::ranges::fold_left(view, 0, std::plus<>{});

// std::views::common - 转换为 common_range
// 某些视图(如 iota)迭代器类型不同,需要转换才能用于某些算法
auto common_view = infinite | std::views::take(100) | std::views::common;

// std::ranges::to_vector (C++23) - 直接收集到容器
auto vec = view | std::ranges::to_vector;

实战 4:处理 Qt 容器

cpp 复制代码
#include <ranges>
#include <QList>
#include <QString>
#include <QDebug>

// Qt 容器天然支持 std::ranges,因为 QList 提供了 begin/end
void processQtContainers() {
    QList<QString> names = {"Alice", "Bob", "Charlie", "Diana", "Eve"};
    
    // 过滤长度大于4的名字
    auto longNames = names 
        | std::views::filter([](const QString& s) { 
            return s.length() > 4; 
        });
    
    // 转换为大写
    auto upperNames = longNames 
        | std::views::transform([](const QString& s) { 
            return s.toUpper(); 
        });
    
    qDebug() << "Long names (uppercase):";
    for (const QString& name : upperNames) {
        qDebug() << " " << name;
    }
    // 输出: "CHARLIE" "DIANA"
    
    // 与 QML 列表视图集成
    // 假设你有一个 QStringList 需要传递给 QML
    QStringList qmlList;
    for (const QString& name : upperNames) {
        qmlList << name;
    }
    // 只需要遍历一次,惰性求值确保内存效率
    
    // 组合多个 Qt 容器
    QList<int> ages = {25, 17, 30, 22, 28};
    QList<QString> filteredWithAges;
    
    // ⚠️ std::views::zip_transform 是 C++23 特性!
// 在 C++20 中不可用

// C++20 兼容方式:使用 boost::zip 或手动迭代
std::vector<QString> zipped;
auto it1 = names.begin();
auto it2 = ages.begin();
for (; it1 != names.end() && it2 != ages.end(); ++it1, ++it2) {
    zipped.push_back(QString("%1 (%2)").arg(*it1).arg(*it2));
}

// 或者使用 C++23 的 zip_transform 需要 <ranges> 头文件支持
// #include <ranges>
// auto zipped = std::views::zip_transform(
//     [](const QString& name, int age) {
//         return QString("%1 (%2)").arg(name).arg(age);
//     },
//     names, ages
// );
    
    for (const QString& info : zipped) {
        qDebug() << info;
    }
}

// 技巧:使用 views::iota 生成 QList 索引
void indexedProcessing() {
    QList<QString> items = {"A", "B", "C", "D", "E"};
    
    // 注意:transform 中的 items[i] 每次迭代都会执行索引访问
// 对于 QList,operator[] 是 O(1) 的,所以没问题

auto indexed = std::views::iota(0) 
    | std::views::take(items.size())
    | std::views::transform([&](int i) {
        return QString("[%1]: %2").arg(i).arg(items[i]);  // QList 的 operator[] O(1)
    });

// 但需要注意:如果 items 在 transform 执行期间被修改,行为是未定义的
// 因为 transform_view 是惰性的,items 的修改会反映在结果中

// 更安全的方式:先复制索引或使用 QList 的迭代器
auto indexed_safe = std::views::iota(0) 
    | std::views::take(items.size())
    | std::views::transform([items = items](int i) {  // 捕获副本
        return QString("[%1]: %2").arg(i).arg(items[i]);
    });
    
    for (const QString& item : indexed) {
        qDebug() << item;
    }
}

常见陷阱与最佳实践

陷阱 1:View 被销毁后继续使用迭代器

cpp 复制代码
// ❌ 危险:返回指向临时视图的迭代器
auto getEvens() {
    std::vector<int> data = {1, 2, 3, 4, 5};
    auto evens = data | std::views::filter([](int n) { return n % 2 == 0; });
    return evens.begin();  // 危险!evens 在函数结束时销毁
}

// ✅ 正确:返回容器或使用 span
auto getEvens() -> std::vector<int> {
    std::vector<int> data = {1, 2, 3, 4, 5};
    std::vector<int> result;
    for (int n : data | std::views::filter([](int n) { return n % 2 == 0; })) {
        result.push_back(n);
    }
    return result;  // 返回副本
}

// ❌ 不可行:filter_view 不能直接转为 span
// 因为 span 要求数据是连续存储的,而 filter_view 是惰性的

// ✅ 正确:返回实际容器(或使用 shared_ptr 等)
std::vector<int> getEvens(const std::vector<int>& data) {
    std::vector<int> result;
    for (int n : data | std::views::filter([](int n) { return n % 2 == 0; })) {
        result.push_back(n);
    }
    return result;  // NRVO/RVO 优化,零拷贝
}

// ✅ 正确:接受输出迭代器(类似标准库风格)
template <typename OutputIt>
void getEvens(const std::vector<int>& data, OutputIt out) {
    for (int n : data | std::views::filter([](int n) { return n % 2 == 0; })) {
        *out++ = n;
    }
}

陷阱 2:惰性求值导致意外行为

cpp 复制代码
std::vector<int> data = {1, 2, 3, 4, 5};

// ❌ 错误:每次迭代都重新应用所有转换
auto bad = data | std::views::filter([](int n) {
    std::cout << "filter " << n << "\n";
    return n % 2 == 0;
});

// 如果多次遍历同一视图,filter 会被调用多次
for (int n : bad) std::cout << "got " << n << "\n";  // filter 被调用5次
for (int n : bad) std::cout << "got " << n << "\n";  // filter 又被调用5次!

// ✅ 正确:先物化(materialize)到容器
std::vector<int> materialized;
std::ranges::copy(bad, std::back_inserter(materialized));

// C++23 更简洁的方式:
// auto materialized = bad | std::ranges::to<std::vector<int>>();

// ⚠️ 注意:物化会消耗额外的内存,适用于:
// - 需要多次遍历同一数据
// - 需要随机访问
// - 需要在不同线程间传递数据
// 
// 如果只需要单次遍历,直接使用 view 更高效(零拷贝)
for (int n : materialized) std::cout << "got " << n << "\n";  // 只计算一次
for (int n : materialized) std::cout << "got " << n << "\n";  // 使用缓存

陷阱 3:无限视图导致的死循环

cpp 复制代码
// ❌ 危险:iota 默认生成无限序列
auto naturals = std::views::iota(0);  // 无限序列!

// 这些不会终止(除非 take/break)
// auto evens = naturals | std::views::filter([](int n) { return n % 2 == 0; });
// for (int n : evens) { }  // 死循环!

// ✅ 正确:总是用 take 或 break 限制
auto first100 = std::views::iota(0) | std::views::take(100);

// ✅ 正确:使用 break 条件退出
for (int n : naturals) {
    if (n > 1000) break;  // 手动终止
}

陷阱 4:View 不能直接用于需要容器的场景

cpp 复制代码
std::vector<int> data = {3, 1, 4, 1, 5, 9, 2, 6};

// ❌ 错误:view 没有 .size() 方法(某些 view 支持 size())
auto filtered = data | std::views::filter([](int n) { return n > 3; });
// size_t len = filtered.size();  // 编译错误!filter_view::size() 需要 O(1)
// filtered.size() 可能不存在或需要 O(n) 计算

// ✅ 正确:使用 ranges::distance
size_t len = std::ranges::distance(filtered);  // 可以工作,但可能 O(n)

// ✅ 更好:需要 size 时物化到容器
std::vector<int> materialized(filtered.begin(), filtered.end());
size_t len = materialized.size();  // O(1)

陷阱 5:管道操作符优先级

cpp 复制代码
std::vector<int> data = {1, 2, 3, 4, 5};

// ⚠️ 注意:管道操作符是左结合的
// data | filter(pred) | transform(fn) | take(3)
// 解析为:(((data | filter(pred)) | transform(fn)) | take(3))
// 这个解析顺序是符合预期的,不需要额外括号

// 真正需要注意的优先级问题:
// ❌ 错误:在复杂表达式中,函数调用优先级高于管道
auto wrong = data | std::views::filter([](int n) { return n > 2; })
                  | std::views::transform([](int n) { return n * 2; })
                  | std::views::take(3 + 1);  
// 这里的 3 + 1 会先计算为 4,所以结果是 take(4)
// 这不会导致语法问题,但可读性差

// ✅ 正确:使用括号使意图更清晰
auto correct = data 
    | std::views::filter([](int n) { return n > 2; })
    | std::views::transform([](int n) { return n * 2; })
    | std::views::take(4);

// ✅ 正确:使用括号明确优先级
auto correct = data 
    | std::views::filter([](int n) { return n > 2; })
    | std::views::transform([](int n) { return n * 2; })
    | std::views::take(3);  // take(3),明确

// ✅ 推荐:复杂表达式提取为变量
auto isEven = [](int n) { return n % 2 == 0; };
auto doubleIt = [](int n) { return n * 2; };
auto positive = [](int n) { return n > 0; };

auto result = data | std::views::filter(isEven) 
                  | std::views::transform(doubleIt)
                  | std::views::filter(positive);

最佳实践 1:View 组合命名

cpp 复制代码
// ✅ 好的实践:为复杂管道定义语义化名称
auto getAdultUsers = std::views::filter([](const User& u) {
    return u.age >= 18;
});

auto getActiveUsers = std::views::filter([](const User& u) {
    return u.active;
});

auto sortByAge = std::views::transform([](const User& u) {
    return std::pair{u.age, u.name};
});

// 业务逻辑组合
auto adultActiveUsersSorted = g_users
    | getAdultUsers
    | getActiveUsers
    | sortByAge;

最佳实践 2:调试惰性视图

cpp 复制代码
// ✅ 添加日志的技巧
auto debugView = [](auto view, const char* name) {
    return view | std::views::transform([=](auto&& elem) {
        std::cout << name << ": " << elem << "\n";
        return std::forward<decltype(elem)>(elem);
    });
};

// 使用
auto result = data
    | debugView(std::views::filter(pred), "filter")
    | debugView(std::views::transform(fn), "transform")
    | std::views::take(10);

进阶思考

1. View 的复杂度保证

Ranges 库对 View 有严格的复杂度保证:

操作 复杂度要求
构造 View O(1)
拷贝/赋值 View O(1)
begin() / end() O(1)
size() (如果支持) O(1)

这确保了 View 的"零拷贝、零抽象惩罚"特性。

2. View 与算法复杂度的关系

cpp 复制代码
// Ranges 不会改变算法的复杂度
// filter + transform 仍然是 O(n)

// 但可以优化内存使用:
// 传统:需要多个临时容器
std::vector<int> temp1, temp2, temp3;
for (int n : data) if (pred1(n)) temp1.push_back(n);
for (int n : temp1) temp2.push_back(fn1(n));
for (int n : temp2) if (pred2(n)) temp3.push_back(n);
// 空间复杂度: O(3n)

// Ranges:单次遍历,多个适配器组合
auto result = data 
    | std::views::filter(pred1)
    | std::views::transform(fn1)
    | std::views::filter(pred2);
    // 空间复杂度: O(1) - 不需要临时容器
    // 时间复杂度: O(n) - 单次遍历

3. 自定义 View Adaptor 的完整模板

cpp 复制代码
#include <ranges>
#include <concepts>
#include <iterator>

// 注意:自定义 view 类通常需要继承 view_interface 以自动实现
// size()、empty()、data() 等成员函数

// Step 1: 定义自定义 view 类
template <std::ranges::viewable_range V>
    requires std::ranges::view<V>  // 要求 V 本身是 view
class my_view : public std::ranges::view_interface<my_view<V>> {
private:
    V base_;  // 底层 range(必须是 view)
    
public:
    // 默认构造
    my_view() requires std::default_initializable<V> = default;
    
    // 从 range 构造(接收 view)
    constexpr explicit my_view(V base) : base_(std::move(base)) {}
    
    // 基 range 访问
    constexpr V base() const& requires std::copyable<V> { return base_; }
    constexpr V base() && { return std::move(base_); }
    
    // 迭代器类型(需要实现)
    struct iterator {
        // ... 迭代器实现
    };
    
    // begin/end
    constexpr auto begin() { /* 返回迭代器 */ }
    constexpr auto end() { /* 返回哨兵 */ }
};

// Step 2: 定义 adaptor 函数对象
struct my_adaptor {
    template <std::ranges::viewable_range R>
    constexpr auto operator()(R&& r) const {
        // 确保传入的是 view
        auto view = std::views::all(std::forward<R>(r));
        return my_view<decltype(view)>(std::move(view));
    }
};

// Step 3: 绑定到 std::views 命名空间(可选)
namespace std::views {
    inline constexpr my_adaptor my;
}

4. Ranges 对 C++ 标准库的影响

cpp 复制代码
// C++20 起,所有算法都有 ranges 版本
namespace std::ranges {
    // 算法<算法名>
    template <range R, class T>
    constexpr T find(R&& r, const T& value);
    
    template <range R, class P>
    constexpr std::ranges::iterator_t<R> find_if(R&& r, P pred);
    
    template <range R, class T>
    constexpr iota_result min(R&& r, T value);
    
    // 还有更多...
}

// 使用新算法
auto it = std::ranges::find(container, target);
auto it = std::ranges::find_if(container, pred);

// ranges 算法可以直接接受 view,不需要 begin/end
auto it = std::ranges::find(data | filter_view, target);

参考资源

标准文档

权威书籍

  • 《C++20 Ranges Guide》 - Barry Revzin (Ranges 库设计者之一)
  • 《Modern C++ Programming Cookbook》 - 涵盖 Ranges 实战
  • 《Mastering the C++17 STL》 - 为学习 Ranges 打基础

高质量博客

Qt 集成资源


下期预告:Day 11 - C++20 Ranges(下):视图工厂、视图转换、算法与Ranges结合、C++23 Ranges 新特性

相关推荐
有点。1 小时前
C++深度优先搜索(二)
开发语言·c++·深度优先
二进制杯莫停1 小时前
A和B环境的python版本相同,B环境无法安装pip依赖,离线安装
开发语言·python·pip
l1t2 小时前
kryonix提交的DuckDB 统一并优化标量执行器基础设施 - #24564 PR
开发语言·数据库·数据仓库·sql
unityのkiven3 小时前
C# 中的奇异递归模板模式:MonoSingleton<T> 的实现
java·开发语言·c#
就不掉头发3 小时前
C++函数模板
java·开发语言·c++
我是苏苏3 小时前
WPF开发01:基础控件及常用模板篇
开发语言·c#·html·wpf
wuyk5553 小时前
89.嵌入式内存管理的“稳定利器”:内存池原理与C语言实现
c语言·开发语言·stm32·单片机·嵌入式硬件
mifengxing4 小时前
Java TreeSet
java·开发语言
m0_519196404 小时前
【设计模式】java的习题
开发语言·python