<ranges> 视图类

“视图”是一个轻量级范围,它引用它不拥有的元素(owning_view 除外)。 视图通常基于另一个范围,并且通过转换或筛选提供不同的查看方式。 例如,,std::views::filter 是一个视图,它使用指定的条件从另一个范围中选择元素。

访问视图中的元素时,它是“惰性”完成的,因此只有当你获取元素时才完成工作。 这样就可合并或组合视图,而不会造成性能损失。

例如,可以创建一个视图,该视图仅提供区域中的偶数元素,然后通过对它们进行平方来转换它们。 仅对你访问的元素,并且仅在你访问这些元素时执行筛选和转换。

无论视图包含多少个元素,都可以在固定时间内复制、分配和销毁视图。 这是因为视图不拥有它引用的元素,因此不需要创建副本。 这就是为什么你可以在没有性能损失的情况下组合视图。

通常使用范围适配器来创建视图。 范围适配器是创建视图的预期方法,比直接实例化视图类更容易使用,有时比直接实例化视图类更有效。 如果需要基于现有视图类型创建自己的自定义视图类型,则直接公开视图类。

下面的简短示例演示了如何创建一个矢量中可被 3 整除的元素的平分的视图:

// requires /std:c++20 or later
#include <ranges>
#include <vector>
#include <iostream>

int main()
{
    std::vector<int> input =  {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    auto divisible_by_three = [](const int n) {return n % 3 == 0;};
    auto square = [](const int n) {return n * n;};

    auto x = input
             | std::views::filter(divisible_by_three)
             | std::views::transform(square);

    for (int i : x)
    {
        std::cout << i << ' '; // 0 9 36 81
    }
}
0 9 36 81

在视图基于的范围被修改后使用视图可能会导致未定义的行为。 例如,如果添加或移除基础矢量中的元素,不应重复使用基于矢量的 reverse_view。 修改基础矢量会使容器的 end 迭代器失效,包括使视图可能已创建的迭代器的副本失效。

视图创建成本低,因此如果修改基础范围,通常应重新创建视图。 以下示例演示如何将视图管道存储在变量中,以便可重用它。

// requires /std:c++20, or later
#include <iostream>
#include <ranges>
#include <vector>
#include <list>
#include <string_view>
#include <algorithm>

template<typename rangeType>
void show(std::string_view msg, rangeType r)
{
    std::cout << msg;
    std::ranges::for_each(r,
        [](auto e)
        {
            std::cout << e << ' ';
        });
    std::cout << '\n';
}

int main()
{
    std::vector v{ 1, 2, 3, 4 };
    show("v: ", v);

    // You can save a view pipeline
    auto rev3 = std::views::take(3) | std::views::reverse;

    show("v | rev3: ", v | rev3); // 3 2 1

    v.insert(v.begin(), 0); // v = 0 1 2 3 4
    show("v: ", v);

    // Because modifying the vector invalidates its iterators, rebuild the view.
    // We are reusing the view pipeline we saved earlier
    show("v | rev3(v): ", rev3(v));
}
v: 1 2 3 4
v | rev3: 3 2 1
v: 0 1 2 3 4
v | rev3(v): 2 1 0

以下视图类在 std::ranges 命名空间中定义。

查看 说明
basic_istream_viewC++20 输入流中连续元素的视图。 专用化包括 istream_viewwistream_view
common_viewC++20 将具有不同迭代器/sentinel 类型的视图调整为具有相同迭代器/sentinel 类型的视图。
drop_viewC++20 从另一个视图创建,跳过第一个 count 元素。
drop_while_viewC++20 从另一个视图创建,只要保留谓词,就跳过前导元素。
elements_viewC++20 集合中每个类似元组的值的所选索引的视图。 例如,给定一系列 std::tuple<string, int> 值后,创建一个视图,其中包含每个元组中的所有 string 元素。
empty_viewC++20 没有元素的视图。
filter_viewC++20 筛选掉范围中与谓词不匹配的元素。
iota_viewC++20 生成的包含一些列递增值的视图。
join_viewC++20 将多个范围的所有元素合并为一个视图。
keys_viewC++20 集合中每个类似元组的值的第一个索引的视图。 例如,给定一系列 std::tuple<string, int> 值后,创建一个视图,其中包含每个元组中的 string 元素。
lazy_split_viewC++20 根据分隔符将视图拆分为子范围。
owning_viewC++20 从另一个范围获取元素的所有权。
ref_viewC++20 引用属于其他范围的元素的视图。
reverse_viewC++20 以倒序呈现范围的元素。
single_viewC++20 仅包含一个元素的视图。
split_viewC++20 根据分隔符将视图拆分为子范围。
subrangeC++20 范围的部分元素的视图,由开始迭代器和 sentinel 定义。
take_viewC++20 包含从范围前面获取的指定数量的元素。
take_while_viewC++20 包含范围中与给定谓词匹配的前导元素。
transform_viewC++20 将转换函数应用于每个元素后的基础序列的视图。
values_viewC++20 集合中每个类似元组的值的第二个索引的视图。 例如,给定一系列 std::tuple<string, int> 值后,创建一个视图,其中包含每个元组中的 int 元素。

其中很多类在创建其实例的 std:views 命名空间具有相应的范围适配器。 首选使用适配器来创建视图,而不是直接创建视图类。 范围适配器是创建视图的预期方法,它们更易于使用,而且在某些情况下更加高效。

视图类特征

每个视图类主题在语法节之后都有一个“特征”部分。 “特征”部分包含以下条目:

  • 范围适配器:指向创建视图的范围适配器的链接。 通常使用范围适配器来创建视图,而不是直接创建视图类,因此为了方便起见,这里列出了它。

  • 基础范围:视图对其可使用的基础范围类型有不同的迭代器要求。 有关迭代器类型的详细信息,请参阅范围迭代器层次结构

  • 查看迭代器类别:视图的迭代器类别。 当视图调整范围时,视图的迭代器类型通常与基础范围的迭代器类型相同。 但是,对于某些视图,它可能有所不同。 例如,reverse_view 有一个 bidirectional_iterator,即使基础范围有一个 random_access_iterator 也是如此。

  • 元素类型:视图迭代器返回的元素的类型。

  • 已设置大小:视图是否可返回它引用的元素数。 并非所有视图都可以。

  • 常见范围:指定视图是否为 common_range,这意味着开始迭代器和 sentinel 类型是相同的。 对于适用于迭代器对的预范围代码,常见范围非常有用。 一个例子是序列容器的迭代器对构造函数,例如 vector(ranges::begin(x), ranges::end(x))

  • 借用范围:指定视图是否为借用范围。 borrowed_range<T> 意味着你可在 T 销毁后使用 T 的迭代器。

    没有标准容器就是一个借用范围,因为销毁容器会释放元素并使任何迭代器失效。 在这种情况下,我们说迭代器在销毁后被“悬空”。

    例如,std::ranges::find() 通常向 range 参数中的 found 元素返回迭代器。 如果 range 参数是临时 (rvalue) 容器,则存储返回的迭代器并稍后使用它是错误的,因为它是“悬空”的。

    返回迭代器(或子范围)的范围算法仅在其参数是 lvalue(非临时)或借用范围时才这样做。 否则,它们返回一个 std::dangling 对象,如果你尝试像使用迭代器一样使用该对象,它会在错误消息中提示什么出现了错误。

  • const 可迭代:指示是否可循环访问视图的 const 实例。 并非所有 const 视图都可以迭代。 如果视图不是 const 可迭代的,你无法迭代 for (const auto& element : as_const(theView)),也无法将其传递给函数,其中该函数接受对视图的 const 引用,然后尝试循环访问它。

范围迭代器层次结构

在每个视图类主题的“特征”部分中,基础范围和视图迭代器类别中的迭代器类别是指范围/视图支持的迭代器类型。 有 6 类范围迭代器,它们由 C++20 概念标识。 按功能的增加顺序,范围迭代器的层次结构如下:

范围迭代器概念 说明
output_range 只写,仅向前移动;单传递。
input_range 只读,仅向前移动;单传递。
forward_range 仅向前移动;多传递。
bidirectional_range 可向前和向后移动;多传递。
random_access_range 可使用索引访问集合;多传递。
contiguous_range 可使用索引访问集合,元素在内存中连续存储。

一般来说,迭代器具有表中它前面的迭代器的功能。 例如,bidirectional_range 具有 forward_range 功能,但反之并非是这样。 input_range 除外,它没有 output_range 的功能,因为你无法写入 input_range

“需要 input_range 或更高范围”表述意味着视图可以与 input_rangeforward_rangebidirectional_rangerandom_access_rangecontiguous_range 迭代器一起使用,因为它们在功能上与 input_range 相同。

范围迭代器层次结构与迭代器层次结构直接相关。 有关详细信息,请参阅迭代器概念

另请参阅

<ranges>
范围适配器