<ranges> 概念

概念是 C++20 的一项语言功能,可在编译时约束模板参数。 它们有助于防止模板实例化不正确,在可读表单中指定模板自变量要求,并提供更简洁的模板相关编译器错误。

请考虑以下示例,该示例定义了一个概念,用于防止使用不支持除法的类型实例化模板:

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

// Definition of dividable concept which requires 
// that arguments a & b of type T support division
template <typename T>
concept dividable = requires (T a, T b)
{
    a / b;
};

// Apply the concept to a template.
// The template will only be instantiated if argument T supports division.
// This prevents the template from being instantiated with types that don't support division.
// This could have been applied to the parameter of a template function, but because
// most of the concepts in the <ranges> library are applied to classes, this form is demonstrated.
template <class T> requires dividable<T>
class DivideEmUp
{
public:
    T Divide(T x, T y)
    {
        return x / y;
    }
};

int main()
{
    DivideEmUp<int> dividerOfInts;
    std::cout << dividerOfInts.Divide(6, 3); // outputs 2
    // The following line will not compile because the template can't be instantiated 
    // with char* because char* can be divided
    DivideEmUp<char*> dividerOfCharPtrs; // compiler error: cannot deduce template arguments 
}

将编译器开关 /diagnostics:caret 传递给 Visual Studio 2022 版本 17.4 预览版 4 或更高版本时,概念 dividable<char*> 计算结果为 false 的错误将直接指向失败的表达式要求 (a / b)

范围概念在命名空间 std::ranges 中定义,并在头文件 <ranges> 中声明。 它们用于范围适配器视图等的声明。

范围有六种类别。 它们与<iterator>概念中列出的迭代器类别相关。 这些类别如下所示(按功能逐渐增强的顺序):

范围概念 说明
output_range
input_range
指定可以写入的范围。
指定可以进行一次读取的范围。
forward_range 指定可以进行多次读取(可能可以写入)的范围。
bidirectional_range 指定可以向前和向后读取和写入的范围。
random_access_range 指定可以按索引读取和写入的范围。
contiguous_range 指定一个范围,其元素在内存中按顺序排列,大小相同,并且可以使用指针算术进行访问。

在上表中,按功能逐渐增强的顺序列出概念。 满足概念要求的范围通常满足其前面行中概念的要求。 例如,random_access_range 具有 bidirectional_rangeforward_rangeinput_rangeoutput_range 功能。 input_range 例外,它无法写入,因此没有 output_range 功能。

范围迭代器层次结构的关系图。input_range和output_range是最基本的迭代器。接下来forward_range,并优化input_range和output_range。bidirectional_range优化forward_range。random_access_range优化bidirectional_range。最后,contiguous_range优化random_access_range

其他范围概念包括:

范围概念 说明
rangeC++20 指定提供迭代器和 sentinel 的类型。
borrowed_rangeC++20 指定范围的迭代器生存期不与范围的生存期相关。
common_rangeC++20 指定范围迭代器的类型与范围 sentinel 的类型相同。
Simple_ViewC++20 不是定义为标准库的一部分的正式概念,而是在某些接口上用作帮助程序概念。
sized_rangeC++20 指定一个可以有效提供其元素数量的范围。
viewC++20 指定具有高效(常数时间)移动构造、赋值和销毁的类型。
viewable_rangeC++20 指定一个类型,可以是视图,或者可以转换为视图。

bidirectional_range

bidirectional_range 支持向前和向后读取和写入范围。

template<class T>
concept bidirectional_range =
    forward_range<T> && bidirectional_iterator<iterator_t<T>>;

参数

T
要测试以了解它是否为 bidirectional_range 的类型。

备注

此类范围支持 bidirectional_iterator 或更高版本。 bidirectional_iterator 具有 forward_iterator 功能,但也可以向后循环访问。

bidirectional_range 的一些示例包括 std::setstd::vectorstd::list

borrowed_range

如果从对象获取的迭代器的有效性可能大于对象的生存期,则类型遵循 borrowed_range 模型。 也就是说,即使范围不再存在,也可以使用其迭代器。

template<class T>
concept borrowed_range =
    range<T> &&
    (is_lvalue_reference_v<T> || enable_borrowed_range<remove_cvref_t<T>>);

参数

T
要测试以了解它是否为 borrowed_range 的类型。

备注

值范围的生存期可以在函数调用后结束,无论范围是否遵循 borrowed_range 模型。 如果是 borrowed_range,则无论范围的生存期何时结束,都可以继续使用具有明确定义的行为的迭代器。

例如,对于容器(如 vectorlist),情况则有所不同,因为容器的生存期结束时,迭代器将引用已销毁的元素。

可以继续对 borrowed_range(例如 iota_view<int>{0, 42} 之类的 view)使用迭代器,它们的迭代器基于一组值,这些值不会被销毁,因为它们是按需生成的。

common_range

common_range 的迭代器类型与 sentinel 类型相同。 也就是说,begin()end() 会返回相同的类型。

template<class T>
concept common_range =
   ranges::range<T> && std::same_as<ranges::iterator_t<T>, ranges::sentinel_t<T>>;

参数

T
要测试以了解它是否为 common_range 的类型。

备注

std::ranges::begin()std::ranges::end() 中获取类型对于计算两个迭代器之间的距离的算法以及接受迭代器对表示的范围的算法非常重要。

标准容器(例如,vector)满足 common_range 的要求。

contiguous_range

contiguous_range 元素按顺序存储在内存中,并且可以使用指针算术访问。 例如,数组是一个 contiguous_range

template<class T>
concept contiguous_range =
    random_access_range<T> && contiguous_iterator<iterator_t<T>> &&
    requires(T& t) {{ ranges::data(t) } -> same_as<add_pointer_t<range_reference_t<T>>>;};

参数

T
要测试以了解它是否为 contiguous_range 的类型。

备注

可以通过指针算术来访问 contiguous_range,因为元素按顺序在内存中排列并且大小相同。 这种范围支持 continguous_iterator,这是所有迭代器中最灵活的。

contiguous_range 的一些示例包括 std::arraystd::vectorstd::string

示例: contiguous_range

以下示例演示如何使用指针算术访问 contiguous_range

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

int main()
{
    // Show that vector is a contiguous_range
    std::vector<int> v = {0,1,2,3,4,5};
    std::cout << std::boolalpha << std::ranges::contiguous_range<decltype(v)> << '\n'; // outputs true

    // Show that pointer arithmetic can be used to access the elements of a contiguous_range
    auto ptr = v.data();
    ptr += 2;
    std::cout << *ptr << '\n'; // outputs 2
}
true
2

forward_range

forward_range 支持多次读取(可能可以写入)范围。

template<class T>
concept forward_range = input_range<T> && forward_iterator<iterator_t<T>>;

参数

T
要测试以了解它是否为 forward_range 的类型。

备注

此类范围支持 forward_iterator 或更高版本。 forward_iterator 可以多次循环访问范围。

input_range

input_range 是可进行一次读取的范围。

template<class T>
concept input_range = range<T> && input_iterator<iterator_t<T>>;

参数

T
要测试以了解它是否为 input_range 的类型。

备注

当类型满足 input_range 的要求时:

  • ranges::begin() 函数将返回 input_iterator。 对 input_range 多次调用 begin() 会导致未定义的行为。
  • 可以重复取消引用 input_iterator,每次都会生成相同的值。 input_range 不是多通道。 递增迭代器会使任何副本失效。
  • 它可以与 ranges::for_each 一起使用。
  • 它支持 input_iterator 或更高版本。

output_range

output_range 是可以写入的范围。

template<class R, class T>
concept output_range = range<R> && output_iterator<iterator_t<R>, T>;

参数

R
范围的类型。

T
要写入范围的数据类型。

备注

output_iterator<iterator_t<R>, T> 是一个类型,它提供一个迭代器,可以将类型 T 的值写入类型 R 的范围。 换句话说,它支持 output_iterator 或更高版本。

random_access_range

random_access_range 可以按索引读取或写入范围。

template<class T>
concept random_access_range =
bidirectional_range<T> && random_access_iterator<iterator_t<T>>;

参数

T
要测试以了解它是否为 sized_range 的类型。

备注

此类范围支持 random_access_iterator 或更高版本。 random_access_range 具有 input_rangeoutput_rangeforward_rangebidirectional_range 功能。 random_access_range 是可排序的。

random_access_range 的一些示例包括 std::vectorstd::arraystd::deque

range

定义类型必须满足以作为 range 的要求。 range 提供迭代器和 sentinel,以便可以循环访问其元素。

template<class T>
concept range = requires(T& rg)
{
  ranges::begin(rg);
  ranges::end(rg);
};

参数

T
要测试以了解它是否为 range 的类型。

备注

range 的要求如下:

  • 可以使用 std::ranges::begin()std::ranges::end() 对其进行迭代
  • ranges::begin()ranges::end() 以摊销常数时间运行,并且不修改 range。 摊销常数时间并不意味着 O(1),而是一系列调用的平均成本,即使在最糟糕的情况下,也是 O(n)而不是 O(n^2) 或更高。
  • [ranges::begin(), ranges::end()) 表示有效范围。

Simple_View

Simple_View 是用于某些 ranges 接口的仅限说明的概念。 它未在库中定义。 它仅用于规范,以帮助描述某些范围适配器的行为。

template<class V>
  concept Simple_View = // exposition only
    ranges::view<V> && ranges::range<const V> &&
    std::same_as<std::ranges::iterator_t<V>, std::ranges::iterator_t<const V>> &&
    std::same_as<std::ranges::sentinel_t<V>, std::ranges::sentinel_t<const V>>;

参数

V
要测试以了解它是否为 Simple_View 的类型。

备注

如果以下所有内容均为 true,则视图 VSimple_View

  • V 是视图
  • const V 是范围
  • vconst V 具有相同的迭代器和 sentinel 类型。

sized_range

sized_range 以摊销常数时间提供范围中的元素数。

template<class T>
  concept sized_range = range<T> &&
    requires(T& t) { ranges::size(t); };

参数

T
要测试以了解它是否为 sized_range 的类型。

备注

sized_range 的要求是调用 ranges::size

  • 不修改范围。
  • 返回摊销常数时间中的元素数。 摊销常数时间并不意味着 O(1),而是一系列调用的平均成本,即使在最糟糕的情况下,也是 O(n)而不是 O(n^2) 或更高。

sized_range 的一些示例包括 std::liststd::vector

示例: sized_range

以下示例显示 intvectorsized_range

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

int main()
{
    std::cout << std::boolalpha << std::ranges::sized_range<std::vector<int>> << '\n'; // outputs "true"
}    

view

view 具有常数时间移动构造、赋值和销毁操作,与它具有的元素数无关。 视图不需要复制可构造或可赋值复制,但如果需要,这些操作也必须在常数时间内运行。

由于常数时间要求,你可以高效组合视图。 例如,给定一个调用 input(一个确定数字是否可以被 3 整除的函数)的 int 向量,以及一个计算数字的平方值的函数,语句 auto x = input | std::views::filter(divisible_by_three) | std::views::transform(square); 将有效地生成一个视图,该视图包含输入中可以被 3 整除的数字的平方值。 将视图与 | 连接在一起称为组合视图。 如果某个类型满足 view 概念,则可以有效地组合它。

template<class T>
concept view = ranges::range<T> && std::movable<T> && ranges::enable_view<T>;

参数

T
要测试以了解它是否为视图的类型。

备注

使视图可组合的基本要求是移动/复制成本低廉。 这是因为视图是使用另一个视图组合时移动/复制而成的。 它必须是可移动的范围。

ranges::enable_view<T> 是一种特征,用于声明符合 view 概念的语义要求。 类型可以通过以下方式选择加入:

  • 公开和明确派生自 ranges::view_interface 专业化
  • 公开和明确派生自空类 ranges::view_base,或
  • ranges::enable_view<T> 专业化为 true

选项 1 是首选选项,因为 view_interface 还提供了默认实施,其中保存了一些必须写入的样板代码。

此外,选项 2 比选项 3 简单一点。

选项 3 的优点是,无需更改类型的定义即可使用。

viewable_range

viewable_range 是一个类型,可以是视图,或者可以转换为视图。

template<class T>
  concept viewable_range =
    range<T> && (borrowed_range<T> || view<remove_cvref_t<T>>);

参数

T
要进行测试的类型,以了解它是视图,还是可以转换为视图。

注解

使用 std::ranges::views::all() 将范围转换为视图。

另请参阅

<ranges>
范围适配器
视图类