C++中视图(View)对象适配器

C++20

表格

适配器 (Adapter) 主要功能 简要说明与示例
views::all 将一个容器或范围转换为视图。 这是许多视图操作的基础,确保你正在处理一个视图。 <br>示例: views::all(my_vector)
views::filter 过滤范围中的元素。 只保留那些满足特定谓词(返回 true)的元素。<br>示例: views::filter([](int i){ return i % 2 == 0; }) (只保留偶数)
views::transform 转换范围中的每个元素。 对每个元素应用一个函数,并生成一个包含转换后结果的新视图。 <br>示例: views::transform([](int i){ return i * i; }) (计算每个元素的平方)
views::take 获取范围的前 N 个元素。 创建一个最多只包含原始范围前 N 个元素的视图。 <br>示例: views::take(5) (获取前 5 个元素)
views::drop 跳过范围的前 N 个元素。 创建一个视图,其中不包含原始范围的前 N 个元素。 <br>示例: views::drop(3) (跳过前 3 个元素)
views::reverse 反转范围中的元素顺序。 创建一个与原始范围顺序相反的视图。 <br>示例: views::reverse
views::keys 提取“键-值”对中的键。 对于一个包含类似 std::pairstd::tuple 元素的范围,提取每个元素的第一个成员。<br>示例: views::keys (用于 std::map)
views::values 提取“键-值”对中的值。 对于一个包含类似 std::pairstd::tuple 元素的范围,提取每个元素的第二个成员。<br>示例: views::values (用于 std::map)
views::iota 生成一个整数序列。 生成一个从起始值开始,不断递增的序列。 <br>示例: views::iota(1, 10) (生成序列 1, 2, 3, ..., 9)
views::join 将范围的范围“扁平化”。 将一个包含多个子范围的范围,连接成一个单一的、连续的视图。 <br>示例: views::join (用于 vector<vector<int>>)
views::split 根据分隔符拆分范围。 根据指定的分隔符或分隔符范围,将一个范围拆分成多个子范围。 <br>示例: views::split(' ') (按空格拆分字符串)
views::elements<N> 提取元组或类元组的第 N 个元素。 从一个包含元组(std::tuple, std::pair 等)的范围中,提取每个元组的第 N 个元素(从 0 开始)。<br>示例: views::elements<0> (提取每个元组的第 0 个元素)
views::counted 从迭代器开始获取 N 个元素。 从一个给定的迭代器开始,创建一个包含 N 个元素的视图。 <br>示例: views::counted(my_vector.begin() + 2, 5) (从第 3 个元素开始,取 5 个)

1. views::all

  • 功能 views::all 的核心功能是确保你正在处理一个视图(view)。它接收一个范围(range),并返回一个代表该范围的视图。
  • 使用场景 这个适配器看起来似乎有点多余,因为像 filtertransform 这样的适配器已经可以接受容器(如 std::vector)了。但它的主要价值在于统一接口和明确意图
    1. 处理左值容器:当你有一个像 std::vector my_vec 这样的左值容器时,直接对它使用管道操作符 | 会自动通过 views::all 将其转换为一个视图。例如 my_vec | views::filter(...) 实际上等价于 views::all(my_vec) | views::filter(...)views::all 在这里隐式地工作。
    2. 处理右值:当你有一个临时对象(右值)时,views::all 会取得其所有权并将其存入一个 owning_view 中,从而延长其生命周期以匹配视图的生命周期。
    3. 泛型编程:在编写模板代码时,你不确定传入的 T 是一个容器还是一个视图。使用 views::all(t) 可以保证你接下来处理的一定是一个视图,使代码更健壮。
  • 工作原理
    • 如果输入本身已经是一个视图,views::all 什么也不做,直接返回该视图。
    • 如果输入是一个左值容器(如 std::vector&),views::all 会创建一个 std::ranges::ref_view,这是一个不拥有数据、只持有对原始容器引用的轻量级视图。
    • 如果输入是一个右值容器(如一个临时的 std::vector),views::all 会创建一个 std::ranges::owning_view,它会接管(移动)这个临时容器,从而拥有数据。

代码示例C++

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

void print_view(std::ranges::view auto& v) { // 一个只接受视图的函数
    for (const auto& elem : v) {
        std::cout << elem << " ";
    }
    std::cout << std::endl;
}

int main() {
    std::vector<int> numbers = {1, 2, 3};

    // my_view 是一个 std::ranges::ref_view<std::vector<int>>
    auto my_view = std::views::all(numbers);

    std::cout << "通过 views::all 创建的视图: ";
    print_view(my_view);
}

输出:

通过 views::all 创建的视图: 1 2 3

2. views::filter

  • 功能 views::filter 用于“筛选”范围。它接收一个范围和一个谓词(一个返回布尔值的函数),并生成一个只包含原始范围中使谓词返回 true 的元素的新视图。
  • 使用场景 任何你需要根据特定条件从一个集合中挑选一部分元素的场景。例如:
    • 从用户列表中筛选出所有已登录的用户。
    • 从产品列表中筛选出所有库存大于零的商品。
    • 从数字列表中筛选出所有的奇数、偶数或素数。
  • 工作原理 filter 的视图和它的迭代器是“智能”的。当你请求下一个元素时,filter 的迭代器会在内部循环遍历原始范围,跳过所有不满足谓词的元素,直到找到第一个满足条件的元素并返回它。这个过程是懒惰的,只在需要时发生。

代码示例C++

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

int main() {
    std::vector<std::string> words = {"apple", "banana", "kiwi", "orange", "grape"};

    // 谓词:筛选出长度大于5的单词
    auto long_words_view = words | std::views::filter([](const std::string& s) {
        return s.length() > 5;
    });

    std::cout << "长度大于5的单词: ";
    for (const auto& word : long_words_view) {
        std::cout << word << " ";
    }
    std::cout << std::endl;
}

输出:

长度大于5的单词: banana orange

3. views::transform

  • 功能 views::transform 用于“转换”或“映射”范围中的每一个元素。它接收一个范围和一个转换函数,并生成一个新视图,新视图中的每个元素都是原始范围中对应元素经过转换函数处理后的结果。
  • 使用场景 当你需要对一个集合中的每个元素执行相同操作并得到一个新集合时。例如:
    • 将一个字符串向量中的所有字符串都转换为大写。
    • 获取一个用户对象列表中的所有用户ID。
    • 计算一个数字列表的平方根或对数。
  • 工作原理 transform 视图的迭代器在被解引用(*it)时,会先获取原始范围的对应元素,然后立即将转换函数应用于该元素,并返回计算结果。返回的结果是一个临时值。

代码示例C++

#include <iostream>
#include <vector>
#include <ranges>
#include <cctype> // for toupper

int main() {
    std::vector<char> letters = {'a', 'b', 'c'};

    // 转换函数:将小写字母转为大写
    auto uppercase_view = letters | std::views::transform([](char c) {
        return static_cast<char>(std::toupper(c));
    });

    std::cout << "转换后的字母: ";
    for (char c : uppercase_view) {
        std::cout << c << " ";
    }
    std::cout << std::endl;
}

输出:

转换后的字母: A B C

4. views::take

  • 功能 views::take 用于从范围的开头获取指定数量的元素。它创建一个视图,该视图最多只包含原始范围的前 N 个元素。如果原始范围的元素数量小于 N,则视图包含所有元素。
  • 使用场景
    • 获取搜索结果的前10项。
    • 处理一个大文件时,先取前几行进行测试。
    • 实现分页功能(例如,每页显示20项)。
  • 工作原理 take 视图在内部维护一个计数器。它的迭代器在移动时会递减这个计数器。当计数器达到零时,迭代器就等于了视图的 end() 迭代器,从而结束迭代。

代码示例C++

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

int main() {
    std::vector<int> numbers = {10, 20, 30, 40, 50, 60};

    // 只取前4个元素
    auto first_four_view = numbers | std::views::take(4);

    std::cout << "前4个数字: ";
    for (int n : first_four_view) {
        std::cout << n << " ";
    }
    std::cout << std::endl;
}

输出:

前4个数字: 10 20 30 40

5. views::drop

  • 功能 views::droptake 相对,它用于跳过范围开头的指定数量的元素,并创建一个包含其余所有元素的视图。
  • 使用场景
    • 去掉文件的标题行或表头。
    • 实现分页时,跳到指定的页面(例如,跳过前 (page_number - 1) * page_size 个项目)。
    • 忽略不重要的前缀数据。
  • 工作原理 drop 视图的 begin() 迭代器被特殊设计过。当你第一次调用 begin() 时,它会立即将内部的迭代器向前移动 N 步(或直到末尾),然后返回这个新位置的迭代器。后续操作都从这个新起点开始。

代码示例C++

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

int main() {
    std::vector<int> numbers = {10, 20, 30, 40, 50, 60};

    // 跳过前2个元素
    auto after_two_view = numbers | std::views::drop(2);

    std::cout << "跳过前2个后的数字: ";
    for (int n : after_two_view) {
        std::cout << n << " ";
    }
    std::cout << std::endl;
}

输出:

跳过前2个后的数字: 30 40 50 60

6. views::reverse

  • 功能 views::reverse 用于创建一个与原始范围元素顺序相反的视图。它要求原始范围必须支持双向迭代(即同时具有 ++-- 操作)。
  • 使用场景
    • 按时间倒序显示日志或帖子。
    • 从后向前处理数据。
  • 工作原理 reverse 视图的 begin() 方法返回一个指向原始范围末尾的反向迭代器 (rbegin()),而它的 end() 方法则返回一个指向原始范围开头的反向迭代器 (rend())。对这些反向迭代器进行 ++ 操作,实际上是在原始范围上进行 -- 操作。

代码示例C++

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

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

    auto reversed_view = numbers | std::views::reverse;

    std::cout << "反转后的序列: ";
    for (int n : reversed_view) {
        std::cout << n << " ";
    }
    std::cout << std::endl;
}

输出:

反转后的序列: 5 4 3 2 1

7. views::keys 和 8. views::values

这两个适配器功能类似,通常一起讲解。它们用于处理由“键-值”对组成的范围,比如 std::mapstd::vector<std::pair>

  • 功能
    • views::keys:提取每个元素中的(第一个成员)。
    • views::values:提取每个元素中的(第二个成员)。
  • 使用场景
    • 当你有一个 std::map,但只关心所有的键或所有的值时。
    • 例如,从 std::map<std::string, double> 中获取所有商品名称(键)或所有价格(值)。
  • 工作原理 它们本质上是 views::elements<0>views::elements<1> 的便捷别名。其 transform 迭代器在解引用时,会获取原始的 pairtuple,然后返回其第0个或第1个成员。

代码示例C++

#include <iostream>
#include <map>
#include <string>
#include <ranges>

int main() {
    std::map<std::string, int> scores = {
        {"Alice", 90},
        {"Bob", 85},
        {"Charlie", 95}
    };

    auto keys_view = scores | std::views::keys;
    std::cout << "所有学生 (keys): ";
    for (const auto& name : keys_view) {
        std::cout << name << " ";
    }
    std::cout << std::endl;

    auto values_view = scores | std::views::values;
    std::cout << "所有分数 (values): ";
    for (int score : values_view) {
        std::cout << score << " ";
    }
    std::cout << std::endl;
}

输出:

所有学生 (keys): Alice Bob Charlie
所有分数 (values): 90 85 95

(注意: std::map 的迭代顺序是按键排序的)


9. views::iota

  • 功能 views::iota 是一个范围工厂,而不是适配器。它用于生成一个等差整数序列。可以指定起始值(必需)和结束值(可选)。如果不提供结束值,它会生成一个无限序列
  • 使用场景
    • 生成用于循环的索引序列,替代传统的 for(int i = 0; ...) 循环。
    • 创建测试用的数字数据。
    • 与其他视图结合,生成复杂序列。
  • 工作原理 iota 视图的迭代器非常轻量,只存储当前的值。每次 ++ 操作只是简单地将内部存储的值加一。解引用 *it 直接返回该值。

代码示例C++

#include <iostream>
#include <ranges>

int main() {
    // 生成从 1 到 9 (不包括10) 的序列
    auto finite_seq = std::views::iota(1, 10);
    std::cout << "有限序列 [1, 10): ";
    for (int i : finite_seq) {
        std::cout << i << " ";
    }
    std::cout << std::endl;

    // 生成从 0 开始的无限序列,并取前5个
    auto infinite_seq = std::views::iota(0) | std::views::take(5);
    std::cout << "无限序列的前5个: ";
    for (int i : infinite_seq) {
        std::cout << i << " ";
    }
    std::cout << std::endl;
}

输出:

有限序列 [1, 10): 1 2 3 4 5 6 7 8 9
无限序列的前5个: 0 1 2 3 4

10. views::join

  • 功能 views::join 用于将一个“范围的范围”(a range of ranges)“扁平化”或“压平”,变成一个单一的、连续的视图。
  • 使用场景
    • 你有一个 std::vector<std::vector<int>>std::vector<std::string>,并希望将其视为一个单一的整数或字符序列进行处理。
    • views::split 结合使用,先拆分再重新组合。
  • 工作原理 join 视图的迭代器比较复杂。它内部维护两个迭代器:一个“外部”迭代器指向当前的子范围(如 vector<int>),一个“内部”迭代器指向该子范围中的当前元素。当内部迭代器到达子范围末尾时,++ 操作会使外部迭代器移至下一个子范围,并重置内部迭代器到新子范围的开头。

代码示例C++

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

int main() {
    std::vector<std::string> sentences = {"Hello world", "C++ Ranges"};

    // 将字符串向量扁平化为字符流
    auto chars_view = sentences | std::views::join;

    std::cout << "扁平化后的字符流: ";
    for (char c : chars_view) {
        std::cout << c;
    }
    std::cout << std::endl;
}

输出:

扁平化后的字符流: Hello worldC++ Ranges

11. views::split

  • 功能 views::split 根据一个分隔符或一个分隔符序列,将一个范围拆分成多个子范围。
  • 使用场景
    • 最典型的场景是按特定字符(如逗号、空格、换行符)拆分字符串。
    • 按特定的子序列(如 "--")分割数据流。
  • 工作原理 split 返回一个由子范围组成的视图。它的迭代器会在原始范围中搜索分隔符。每次找到分隔符,就标志着上一个子范围的结束和下一个子范围的开始。

代码示例C++

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

int main() {
    std::string data = "one,two,three";

    // 按逗号拆分字符串
    auto split_view = data | std::views::split(',');

    std::cout << "按 ',' 拆分字符串:\n";
    for (const auto& subrange : split_view) {
        // 注意:subrange 本身也是一个 range
        // 我们需要再次迭代它或将其转换为 string 来打印
        for(char c : subrange) {
            std::cout << c;
        }
        std::cout << std::endl;
    }
}

输出:

按 ',' 拆分字符串:
one
two
three

12. views::elements<N>

  • 功能 views::elements<N> 从一个由元组(tuple)或类元组(tuple-like,如 std::pair)组成的范围中,提取出每个元组的第 N 个元素(索引从0开始)。
  • 使用场景
    • views::keysviews::values 就是 views::elements<0>views::elements<1> 的特例。
    • 当你有一个 std::vector<std::tuple<std::string, int, double>>,并希望只获取所有元组的第二个元素(int 类型)时。
  • 工作原理 它和 transform 非常相似。它的迭代器在解引用时,获取原始的元组,然后使用 std::get<N> 来提取并返回指定的元素。

代码示例C++

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

int main() {
    std::vector<std::tuple<std::string, int, double>> records = {
        {"CPU", 1, 3.5},
        {"Memory", 16, 3200.0},
        {"SSD", 512, 600.0}
    };

    // 提取每个记录的名称 (第0个元素)
    auto names_view = records | std::views::elements<0>;
    std::cout << "所有设备名称: ";
    for (const auto& name : names_view) {
        std::cout << name << " ";
    }
    std::cout << std::endl;

    // 提取每个记录的数量 (第1个元素)
    auto quantities_view = records | std::views::elements<1>;
    std::cout << "所有设备数量: ";
    for (int quantity : quantities_view) {
        std::cout << quantity << " ";
    }
    std::cout << std::endl;
}

输出:

所有设备名称: CPU Memory SSD
所有设备数量: 1 16 512

13. views::counted

  • 功能 views::counted 从一个给定的迭代器开始,创建一个包含 N 个元素的视图。它和 take 很像,但它的输入是一个迭代器和计数值,而不是一个范围和计数值。
  • 使用场景
    • 当你只有指向范围某个中间位置的迭代器,并想从该位置开始处理固定数量的元素时。
    • 与不完全符合 std::ranges::range 概念但提供迭代器的老旧API或数据结构进行交互。
  • 工作原理 它存储了起始迭代器和计数值。它的行为与 take 视图非常相似,通过内部计数来确定视图的结束。

代码示例C++

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

int main() {
    std::vector<int> numbers = {10, 20, 30, 40, 50, 60, 70};

    // 从第3个元素 (索引为2) 开始
    auto start_iterator = numbers.begin() + 2;

    // 从该迭代器开始,取4个元素
    auto counted_view = std::views::counted(start_iterator, 4);

    std::cout << "从第3个元素开始取4个: ";
    for (int n : counted_view) {
        std::cout << n << " ";
    }
    std::cout << std::endl;
}

输出:

从第3个元素开始取4个: 30 40 50 60

C++ 20/23 补充

表格

适配器 (Adapter) 主要功能 简要说明与示例
views::drop_while 从头开始跳过满足条件的元素。 drop 不同,它会一直跳过元素,直到遇到第一个不满足谓词的元素为止。<br>示例: views::drop_while([](int i){ return i < 5; }) (从头跳过所有小于 5 的数)
views::take_while 从头开始获取满足条件的元素。 take 不同,它会一直获取元素,直到遇到第一个不满足谓词的元素为止。<br>示例: views::take_while([](int i){ return i < 5; }) (从头获取所有小于 5 的数)
views::lazy_split (C++20) 根据分隔符惰性拆分范围。 split 类似,但返回的子范围本身也是视图,并且在被访问时才进行计算。这在处理大范围时更高效。<br>示例: views::lazy_split(',') (按逗号惰性拆分字符串)
views::join_with (C++23) 使用分隔符连接范围的范围。 将一个包含多个子范围的范围“扁平化”,并在原始子范围之间插入一个分隔符范围。 <br>示例: views::join_with('-') (用 - 连接多个字符串子范围)
views::slide (C++23) 创建滑动窗口。 在范围上创建一个固定大小的滑动窗口。每个窗口都是一个包含 N 个连续元素的视图。 <br>示例: views::slide(3) (创建大小为 3 的滑动窗口)
views::chunk (C++23) 将范围划分为固定大小的块。 将范围分割成连续的、不重叠的、大小固定的块(最后一块可能较小)。 <br>示例: views::chunk(3) (将范围按每 3 个元素一块进行划分)
views::stride (C++23) 按固定步长跳跃取元素。 创建一个新视图,其中包含原始范围中每隔 N 个位置的元素。 <br>示例: views::stride(3) (每隔 3 个元素取 1 个,即取第 0, 3, 6, ... 个元素)
views::adjacent<N> (C++23) 创建相邻元素的元组。 创建一个视图,其每个元素都是原始范围中 N 个连续元素的 std::tuple。<br>示例: views::adjacent<3> (将 [1,2,3,4] 变为 [(1,2,3), (2,3,4)])
views::zip (C++23) 将多个范围“压缩”在一起。 将两个或多个范围合并成一个单一的视图。新视图的每个元素是一个 std::tuple,包含来自每个输入范围的对应元素。<br>示例: views::zip(v1, v2)
views::zip_transform (C++23) 压缩并转换多个范围。 先像 zip 一样压缩多个范围,然后对每个生成的元组应用一个函数。<br>示例: views::zip_transform(std::plus<>{}, v1, v2) (对 v1 和 v2 的对应元素求和)
views::cartesian_product (C++23) 计算多个范围的笛卡尔积。 创建一个视图,包含所有输入范围中元素的每一种可能组合(以 std::tuple 形式)。<br>示例: views::cartesian_product(v1, v2)
views::chunk_by (C++23) 根据关系划分范围。 根据一个二元谓词将范围分割成多个块。当相邻两个元素不满足谓词关系时,开启一个新的块。 <br>示例: views::chunk_by(std::ranges::less_equal{}) (按升序序列分块)

14. views::drop_while

  • 功能 views::drop_while 从范围的开头开始,持续跳过所有满足特定谓词的元素,直到遇到第一个不满足谓词的元素为止。它会返回一个包含这个不满足谓词的元素及其之后所有元素的视图。
  • 使用场景
    • 去除数据流开头的所有空格或零值。
    • 在一个按时间排序的日志中,跳过所有在某个特定时间点之前的条目。
    • 跳过文件开头的注释行(例如,所有以 # 开头的行)。
  • 工作原理views::drop 类似,drop_while 主要影响其 begin() 迭代器的行为。当你第一次调用 begin() 时,它会在内部遍历原始范围,对每个元素应用谓词,直到找到第一个返回 false 的元素。然后,begin() 会返回一个指向这个元素的迭代器。视图的所有后续操作都将从这个位置开始。

代码示例

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

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

    // 谓词:跳过所有小于 5 的元素
    auto after_initial_low_numbers = data | std::views::drop_while([](int i) {
        return i < 5;
    });

    std::cout << "跳过开头小于5的数字后: ";
    for (int n : after_initial_low_numbers) {
        std::cout << n << " ";
    }
    std::cout << std::endl;
}

输出:

跳过开头小于5的数字后: 5 4 3 2 1

注意:它在遇到第一个 5 时就停止跳过,即使后面还有小于 5 的数字,它们也会被包含在结果视图中。


15. views::take_while

  • 功能 views::take_while 从范围的开头开始,持续获取所有满足特定谓词的元素,直到遇到第一个不满足谓词的元素为止。它返回一个只包含开头连续满足条件元素的视图。
  • 使用场景
    • 从一个数据流中读取连续的数字或字母前缀。
    • 获取一个有序列表中所有小于某个阈值的初始元素。
    • 在解析文本时,获取第一个单词(即取到第一个空格为止的所有字符)。
  • 工作原理 take_while 视图的迭代器在移动时会检查下一个元素是否满足谓词。如果不满足,这个迭代器就表现得如同到达了范围的末尾(即等于视图的 end() 迭代器),从而终止迭代。

代码示例

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

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

    // 谓词:获取所有连续的偶数
    auto initial_even_numbers = data | std::views::take_while([](int i) {
        return i % 2 == 0;
    });

    std::cout << "开头的连续偶数: ";
    for (int n : initial_even_numbers) {
        std::cout << n << " ";
    }
    std::cout << std::endl;
}

输出:

开头的连续偶数: 2 4 6

注意:它在遇到 7(第一个不满足条件的元素)时就立即停止,后续的偶数 810 不会被包含在内。


16. views::lazy_split

  • 功能 views::lazy_split 的功能与 views::split 几乎完全相同:根据分隔符将一个范围拆分成多个子范围。关键区别在于“懒惰”(lazy)。lazy_split 返回的子范围本身也是视图,其计算被推迟到实际访问时,因此在处理非常大的范围或复杂的子范围时,性能可能更高。
  • 使用场景split 相同,主要用于拆分字符串或数据流。当输入范围非常大,且你可能只需要访问其中几个拆分后的子范围时,lazy_split 的优势尤为明显。
  • 工作原理 lazy_split 创建一个视图,其迭代器在移动时会扫描原始范围以查找分隔符。与 split 的主要区别在于内部实现和返回的子范围类型,它确保了最大程度的懒惰性,避免了不必要的实例化。对于大多数日常使用,其行为和 split 看起来是一样的。

代码示例

#include <iostream>
#include <string_view>
#include <ranges>

int main() {
    std::string_view text = "alpha|beta|gamma";

    auto split_view = text | std::views::lazy_split('|');

    std::cout << "使用 lazy_split 拆分:\n";
    for (const auto& sub : split_view) {
        // sub 是一个 range, 可以直接打印
        std::cout << std::string_view(sub) << std::endl;
    }
}

输出:

使用 lazy_split 拆分:
alpha
beta
gamma

17. views::join_with (C++23)

  • 功能 views::join_with 将一个“范围的范围”扁平化,同时在原始的子范围之间插入一个指定的分隔符。它就像是 join 的增强版。
  • 使用场景
    • 将一个字符串向量用逗号和空格(, )连接成一个单一的字符串。
    • 将分块的数字数据用一个特定的标记序列隔开并连接起来。
  • 工作原理 join_with 的迭代器比 join 更复杂。它不仅需要跟踪外部范围的迭代器和内部范围的迭代器,还需要知道当前是否处于两个子范围之间。如果是,那么迭代器会优先返回分隔符范围中的元素,之后再继续返回下一个子范围的元素。

代码示例

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

int main() {
    std::vector<std::string> words = {"C++23", "Ranges", "Rock"};
    std::string_view separator = " - ";

    // 使用 " - " 作为分隔符连接单词
    auto joined_view = words | std::views::join_with(separator);

    std::cout << "用分隔符连接后: ";
    for (char c : joined_view) {
        std::cout << c;
    }
    std::cout << std::endl;
}

输出:

用分隔符连接后: C++23 - Ranges - Rock

18. views::slide (C++23)

  • 功能 views::slide 在一个范围上创建一系列固定大小的滑动窗口。每个窗口都是一个包含 N 个连续元素的视图。
  • 使用场景
    • 计算移动平均值(Moving Average)。
    • 在信号处理中,对数据进行加窗分析(如傅里叶变换)。
    • 查找数据中所有长度为 N 的连续子序列。
  • 工作原理 slide(N) 返回一个视图,其每个元素本身也是一个视图。这个子视图由 N 个来自原始范围的连续元素构成。当主视图的迭代器移动时,整个窗口在原始范围上向前滑动一个位置。

代码示例

#include <iostream>
#include <vector>
#include <ranges>
#include <numeric> // for std::accumulate

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

    // 创建大小为 3 的滑动窗口
    auto sliding_windows = numbers | std::views::slide(3);

    std::cout << "大小为3的滑动窗口及其和:\n";
    for (const auto& window : sliding_windows) {
        std::cout << "Window: [ ";
        for (int n : window) {
            std::cout << n << " ";
        }
        int sum = std::accumulate(window.begin(), window.end(), 0);
        std::cout << "], Sum: " << sum << std::endl;
    }
}

输出:

大小为3的滑动窗口及其和:
Window: [ 1 2 3 ], Sum: 6
Window: [ 2 3 4 ], Sum: 9
Window: [ 3 4 5 ], Sum: 12
Window: [ 4 5 6 ], Sum: 15

19. views::chunk (C++23)

  • 功能 views::chunk 将一个范围分割成一系列不重叠的、固定大小的(chunks)。除了最后一块,所有块的大小都严格等于 N。如果元素总数不是 N 的倍数,则最后一块会包含剩余的所有元素,其大小会小于 N
  • 使用场景
    • 将数据分批处理(Batch Processing),例如,将一个大的任务列表分成每100个一批,然后分发给工作线程。
    • 按固定的列数格式化输出数据。
  • 工作原理slide 类似,chunk(N) 返回的视图的每个元素也是一个视图(代表一个块)。但与 slide 不同,当主视图的迭代器移动时,它会直接跳到下一个块的起始位置,即向前跳 N 个元素。

代码示例

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

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

    // 将数据按每3个一组分块
    auto chunked_view = numbers | std::views::chunk(3);

    std::cout << "按每3个元素分块:\n";
    int i = 1;
    for (const auto& chunk : chunked_view) {
        std::cout << "Chunk " << i++ << ": [ ";
        for (int n : chunk) {
            std::cout << n << " ";
        }
        std::cout << "]\n";
    }
}

输出:

按每3个元素分块:
Chunk 1: [ 1 2 3 ]
Chunk 2: [ 4 5 6 ]
Chunk 3: [ 7 8 9 ]
Chunk 4: [ 10 ]

20. views::stride (C++23)

  • 功能 views::stride 用于按固定的步长(stride)从一个范围中跳跃式地提取元素,创建一个新视图。它会选取第 0, N, 2N, 3N, ... 个元素。
  • 使用场景
    • 对数据进行降采样(Downsampling)。
    • 处理隔行或隔列存储的数据(例如,从一个扁平化的图像数据中只提取红色通道的值)。
  • 工作原理 stride(N) 视图的迭代器在执行 ++ 操作时,会使其内部指向原始范围的迭代器向前移动 N 步,从而实现跳跃效果。

代码示例

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

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

    // 按步长3提取元素
    auto strided_view = numbers | std::views::stride(3);

    std::cout << "步长为3的采样结果: ";
    for (int n : strided_view) {
        std::cout << n << " ";
    }
    std::cout << std::endl;
}

输出:

步长为3的采样结果: 0 3 6 9

21. views::adjacent<N> (C++23)

  • 功能 views::adjacent<N> 用于创建一系列由 N相邻元素组成的元组(tuple)。它和 slide(N) 非常相似,但 adjacent 的每个元素是 std::tuple,而 slide 的每个元素是一个 range(视图)。
  • 使用场景
    • 当你需要同时访问 N 个相邻元素并进行计算时,且不希望处理子范围的 begin/end,而是想直接通过 std::get 访问。
    • 比较相邻元素,例如检查一个范围是否已排序。
    • 计算差分序列(adjacent<2>)。
  • 工作原理 adjacent<N> 视图的迭代器内部会维护 N 个指向原始范围的迭代器。解引用 (*it) 时,它会用这 N 个迭代器所指向的元素构建一个 std::tuple 并返回。++it 操作会使这 N 个迭代器同时向前移动一步。

代码示例

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

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

    // 创建包含3个相邻元素的元组
    auto adjacent_view = numbers | std::views::adjacent<3>;

    std::cout << "3个相邻元素的元组:\n";
    for (const auto& t : adjacent_view) {
        std::cout << "std::tuple(" << std::get<0>(t)
                  << ", " << std::get<1>(t)
                  << ", " << std::get<2>(t) << ")\n";
    }
}

输出:

3个相邻元素的元组:
std::tuple(1, 1, 2)
std::tuple(1, 2, 3)
std::tuple(2, 3, 5)
std::tuple(3, 5, 8)

22. views::zip (C++23)

  • 功能 views::zip一个或多个范围“压缩”在一起,创建一个新视图。新视图的每个元素是一个 std::tuple,该元组包含了来自所有输入范围的对应位置的元素。视图的长度由最短的输入范围决定。
  • 使用场景
    • 同时迭代两个或多个相关联的数组,例如一个存姓名,一个存年龄。
    • 将两个点集合并,以计算它们之间的成对距离。
  • 工作原理 zip 视图的迭代器内部包含一个来自每个输入范围的迭代器。解引用时,它会分别解引用这些内部迭代器,并将结果收集到一个 std::tuple 中返回。++ 操作会同时递增所有的内部迭代器。

代码示例

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

int main() {
    std::vector<std::string> names = {"Alice", "Bob", "Charlie"};
    std::vector<int> ages = {30, 25, 35};

    auto zipped_view = std::views::zip(names, ages);

    for (const auto& [name, age] : zipped_view) { // 使用结构化绑定
        std::cout << name << " is " << age << " years old.\n";
    }
}

输出:

Alice is 30 years old.
Bob is 25 years old.
Charlie is 35 years old.

23. views::zip_transform (C++23)

  • 功能 views::zip_transformziptransform 的结合体。它首先像 zip 一样将多个范围压缩,然后对每个生成的元组应用一个转换函数,视图的元素是该函数的返回值。
  • 使用场景
    • 计算两个向量的内积(点积)。
    • 将两个分别代表X坐标和Y坐标的列表,合并成一个点(Point)对象列表。
    • 比较两个列表的对应元素是否相等。
  • 工作原理 它的工作方式与 zip 非常相似,但其迭代器在解引用时,在将所有内部迭代器的解引用结果收集到元组后,会立即将该元组作为参数传递给用户提供的转换函数,并返回函数的结果。

代码示例

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

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

    // 转换函数:计算对应元素的和
    auto sum_view = std::views::zip_transform([](int a, int b) {
        return a + b;
    }, v1, v2);

    std::cout << "对应元素的和: ";
    for (int sum : sum_view) {
        std::cout << sum << " ";
    }
    std::cout << std::endl;
}

输出:

对应元素的和: 5 7 9

24. views::cartesian_product (C++23)

  • 功能 views::cartesian_product 用于计算一个或多个范围的笛卡尔积。它创建一个视图,其元素是包含所有输入范围中元素每一种可能组合的 std::tuple
  • 使用场景
    • 生成棋盘上所有的坐标对。
    • 测试函数时,为参数生成所有可能的输入组合。
    • 生成一副扑克牌(花色范围和点数范围的笛卡尔积)。
  • 工作原理 这是最复杂的视图之一。它的迭代器像一个“里程表”或“计数器”。它包含每个输入范围的一个迭代器。当主迭代器 ++ 时,它会递增最后一个范围的迭代器。如果该迭代器到达末尾,就将其重置到开头,并递增倒数第二个范围的迭代器,以此类推。

代码示例

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

int main() {
    std::vector<char> suits = {'H', 'D'}; // Hearts, Diamonds
    std::vector<int> ranks = {1, 2, 3};

    auto deck_view = std::views::cartesian_product(suits, ranks);

    std::cout << "部分牌组的笛卡尔积:\n";
    for (const auto& [suit, rank] : deck_view) {
        std::cout << "Card(" << suit << ", " << rank << ")\n";
    }
}

输出:

部分牌组的笛卡尔积:
Card(H, 1)
Card(H, 2)
Card(H, 3)
Card(D, 1)
Card(D, 2)
Card(D, 3)

25. views::chunk_by (C++23)

  • 功能 views::chunk_by 根据一个二元关系谓词将一个范围分割成多个块。当相邻的两个元素不满足这个关系时,就开启一个新的块。
  • 使用场景
    • 对已排序的数据进行分组。例如,将一个交易列表按用户ID分组。
    • 将一个数字序列按升序或降序子序列进行分块。
    • 按类型将连续存放的相同类型的对象分组。
  • 工作原理 chunk_by 的迭代器在移动时,会查看当前元素和下一个元素。只要它们满足用户提供的二元谓词(例如 std::ranges::equal_to{}),就认为它们在同一个块中。一旦关系不成立,就标志着当前块的结束。

代码示例

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

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

    // 谓词:当后一个元素等于前一个元素时,它们在同一组
    auto chunked_view = data | std::views::chunk_by(std::ranges::equal_to{});

    std::cout << "按相等关系分块:\n";
    for (const auto& chunk : chunked_view) {
        std::cout << "Chunk: [ ";
        for (int n : chunk) {
            std::cout << n << " ";
        }
        std::cout << "]\n";
    }
}

输出:

按相等关系分块:
Chunk: [ 1 1 1 ]
Chunk: [ 2 ]
Chunk: [ 3 3 ]
Chunk: [ 4 4 4 4 ]
Chunk: [ 5 ]