跳至内容

视图、惰性求值与组合

掌握 C++20 视图──轻量级、惰性的范围适配器,通过管道运算符组合。学习 filter、transform、take、drop、split、join、iota,以及如何构建无需中间分配的强大数据管道。

什么是视图?

视图是一种轻量级范围,它具有以下特点:

1. 非拥有 ── 它不拥有它引用的元素
2. O(1) 复制和移动 ── 视图传递成本低
3. 惰性求值 ── 元素在迭代期间按需计算,而非预先计算

视图是范围管道的构建块。不同于在每个步骤创建中间容器,视图组合成对数据的单次遍历:

// Traditional: creates 2 intermediate vectors
auto evens = filter(data, is_even);     // allocation
auto squared = transform(evens, sq);    // allocation

// Views: zero intermediate allocations
auto result = data | views::filter(is_even) | views::transform(sq);
// Nothing computed yet — result is just a view descriptor
for (int x : result) { ... }  // computed element-by-element

视图位于 std::views::(std::ranges::views:: 的命名空间别名)中。

filter 和 transform ── 基本视图

views::filter 保留匹配谓词的元素。views::transform 对每个元素应用一个函数。它们结合起来可以替代大多数显式循环。

filter_transform.cpp
#include <ranges>
#include <vector>
#include <string>
#include <iostream>

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

    // Filter even numbers, then square them
    auto even_squares = numbers
        | std::views::filter([](int x) { return x % 2 == 0; })
        | std::views::transform([](int x) { return x * x; });

    // Nothing has been computed yet — even_squares is a view
    for (int x : even_squares) {
        std::cout << x << ' ';  // 4 16 36 64 100
    }
    std::cout << '\n';

    // Views work with any range — including strings
    std::string text = "Hello, World!";
    auto uppercase = text
        | std::views::filter([](char c) { return std::isalpha(c); })
        | std::views::transform([](char c) { return std::toupper(c); });

    for (char c : uppercase) {
        std::cout << c;  // HELLOWORLD
    }
    std::cout << '\n';

    // Collect view results into a vector (C++23: std::ranges::to)
    std::vector<int> collected;
    for (int x : even_squares) {
        collected.push_back(x);
    }
    // C++23: auto collected = even_squares | std::ranges::to<std::vector>();
}

take、drop 和切片视图

views::take(n) 产生前 n 个元素。views::drop(n) 跳过前 n 个元素。它们结合使用可以实现高效的无复制切片。

take_drop.cpp
#include <ranges>
#include <vector>
#include <iostream>

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

    // take — first N elements
    for (int x : data | std::views::take(3)) {
        std::cout << x << ' ';  // 1 2 3
    }
    std::cout << '\n';

    // drop — skip first N elements
    for (int x : data | std::views::drop(7)) {
        std::cout << x << ' ';  // 8 9 10
    }
    std::cout << '\n';

    // Combine: elements 3-7 (0-indexed: drop 3, take 4)
    for (int x : data | std::views::drop(3) | std::views::take(4)) {
        std::cout << x << ' ';  // 4 5 6 7
    }
    std::cout << '\n';

    // take_while / drop_while — predicate-based
    std::vector<int> sorted_data = {1, 3, 5, 7, 2, 4, 6};

    auto ascending_prefix = sorted_data | std::views::take_while(
        [prev = 0](int x) mutable {
            bool ok = x > prev;
            prev = x;
            return ok;
        });
    for (int x : ascending_prefix) {
        std::cout << x << ' ';  // 1 3 5 7
    }
    std::cout << '\n';

    // Pagination example: page 2, page_size = 3
    int page = 2, page_size = 3;
    auto page_view = data
        | std::views::drop((page - 1) * page_size)
        | std::views::take(page_size);
    for (int x : page_view) {
        std::cout << x << ' ';  // 4 5 6
    }
    std::cout << '\n';
}

split、join 和字符串处理视图

views::split 按分隔符划分范围。views::join 展平嵌套范围。它们对于字符串和数据处理非常强大。

split_join.cpp
#include <ranges>
#include <string>
#include <string_view>
#include <vector>
#include <iostream>

int main() {
    // split — divide a string by delimiter
    std::string csv = "Alice,30,Engineer,95000";

    for (auto field : csv | std::views::split(',')) {
        // Each 'field' is a subrange — convert to string_view
        std::string_view sv(field.begin(), field.end());
        std::cout << '[' << sv << "] ";
    }
    std::cout << '\n';  // [Alice] [30] [Engineer] [95000]

    // join — flatten nested ranges
    std::vector<std::vector<int>> matrix = {
        {1, 2, 3},
        {4, 5, 6},
        {7, 8, 9}
    };
    for (int x : matrix | std::views::join) {
        std::cout << x << ' ';  // 1 2 3 4 5 6 7 8 9
    }
    std::cout << '\n';

    // Combine: split lines, then process each line
    std::string multiline = "hello world\nfoo bar\nbaz qux";
    int line_num = 1;
    for (auto line : multiline | std::views::split('\n')) {
        std::string_view line_sv(line.begin(), line.end());
        std::cout << line_num++ << ": " << line_sv << '\n';
    }

    // Enumerate (C++23)
    // for (auto [i, val] : data | std::views::enumerate) { ... }
}

views::iota ── 生成范围

views::iota 生成一个递增值的序列。它可以是有界的(iota(0, 10) 表示 0..9)或无界的(iota(0) 表示 0, 1, 2, ...)。与 take 结合使用,可以替代许多计数循环。

views_iota.cpp
#include <ranges>
#include <iostream>
#include <vector>
#include <numeric>

int main() {
    // Bounded iota — replaces for(int i = 0; i < 10; ++i)
    for (int x : std::views::iota(0, 10)) {
        std::cout << x << ' ';  // 0 1 2 3 4 5 6 7 8 9
    }
    std::cout << '\n';

    // Unbounded iota + take — first 5 squares
    auto first_5_squares = std::views::iota(1)
        | std::views::transform([](int x) { return x * x; })
        | std::views::take(5);
    for (int x : first_5_squares) {
        std::cout << x << ' ';  // 1 4 9 16 25
    }
    std::cout << '\n';

    // Generate Fibonacci-like sequence with views
    // (iota as index generator)
    auto indices = std::views::iota(0, 20);
    auto even_indices = indices | std::views::filter([](int i) { return i % 2 == 0; });
    for (int i : even_indices | std::views::take(5)) {
        std::cout << i << ' ';  // 0 2 4 6 8
    }
    std::cout << '\n';

    // Use iota to create a vector of sequential values
    auto range_vec = std::views::iota(1, 11);  // 1 to 10
    int sum = 0;
    for (int x : range_vec) sum += x;
    std::cout << "Sum 1..10: " << sum << '\n';  // 55
}

构建完整的数据管道

视图的真正威力在于将多个适配器组合成单个管道。整个管道以惰性方式在单次遍历中求值。

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

struct LogEntry {
    std::string level;   // "INFO", "WARN", "ERROR"
    std::string message;
    int timestamp;
};

int main() {
    std::vector<LogEntry> logs = {
        {"INFO",  "Server started",      1000},
        {"ERROR", "Connection refused",  1001},
        {"INFO",  "Request received",    1002},
        {"WARN",  "Slow query detected", 1003},
        {"ERROR", "Timeout expired",     1004},
        {"INFO",  "Request completed",   1005},
        {"ERROR", "Disk full",           1006},
        {"WARN",  "Memory usage high",   1007},
    };

    // Pipeline: get recent error messages, formatted
    auto recent_errors = logs
        | std::views::filter([](const LogEntry& e) {
              return e.level == "ERROR";
          })
        | std::views::drop(1)            // skip first error
        | std::views::transform([](const LogEntry& e) {
              return "[" + std::to_string(e.timestamp) + "] " + e.message;
          })
        | std::views::take(5);           // at most 5 results

    std::cout << "Recent errors (after first):\n";
    for (const auto& msg : recent_errors) {
        std::cout << "  " << msg << '\n';
    }
    // [1004] Timeout expired
    // [1006] Disk full

    // Reverse view — iterate backward without copying
    auto reversed = logs | std::views::reverse | std::views::take(3);
    std::cout << "\nLast 3 entries (reversed):\n";
    for (const auto& e : reversed) {
        std::cout << "  [" << e.level << "] " << e.message << '\n';
    }
}
常见陷阱

视图是非拥有的──它们引用底层范围。如果源范围被销毁,视图就会变成悬空的:

auto make_view() {
    std::vector<int> local = {1, 2, 3};
    return local | std::views::filter([](int x) { return x > 1; });
    // BUG: local is destroyed, view dangles!
}

也要注意管道中间创建的临时对象的视图:

// WRONG: the string is a temporary that gets destroyed
auto words = std::string("hello world")
    | std::views::split(' ');  // dangling!

始终确保源数据的生命周期长于引用它的所有视图。在创建视图之前,将数据存储在生命周期足够长的变量中。

最佳实践

视图是惰性的──在迭代之前不会执行任何工作。这意味着:

1. 构建管道基本上是免费的(只存储函数指针和参数)
2. 你可以在非常大甚至无限的范围上创建视图
3. 每个元素在下一个元素被处理之前流经整个管道
4. 短路视图如 take(n) 在处理 n 个元素后停止整个管道

要将视图结果具体化到容器中,进行迭代并收集:

std::vector<int> results;
for (int x : some_view) results.push_back(x);

// C++23 adds std::ranges::to for cleaner materialization:
auto results = some_view | std::ranges::to<std::vector>();

尽可能保持数据为视图形式──仅在需要随机访问、持久化或传递给需要容器的 API 时才进行具体化。

关键要点
  • 视图是非拥有的、O(1) 可复制的、惰性求值的范围适配器
  • views::filter 和 views::transform 是数据管道的基本构建块
  • views::take(n) 和 views::drop(n) 提供无需复制的切片操作
  • views::iota 生成序列──有界或无界──替代计数循环
  • 管道运算符 | 将视图组合成可读的、从左到右的管道,零中间分配
  • 视图不能比其源数据存活得更久──始终确保底层范围有足够的生命周期

测验 — 检验你的知识

(20 经验值)

1. C++20 视图何时计算其元素?

2. `std::views::iota(1) | std::views::take(3)` 的结果是什么?

3. 为什么对局部变量创建视图并从函数返回是危险的?