17.4范围(From C++20)
范围库提供的范围是迭代器之上的抽象层,消除了不匹配的迭代器错误,并添加了额外的功能,例如允许范围适配器延迟过滤和转换底层元素序列。在
<ranges>
头文件中定义的范围库由以下主要组件组成。
- 范围:范围是一个概念,它定义了允许元素迭代的类型的要求,任何支持
begin()
和end()
的数据结构都是有效的范围. - 基于范围的算法:接受迭代器的标准库算法大多都有接受范围的等价物
- 投影:很多基于范围的算法都接受所谓的投影回调。这个回调函数会为范围中的每个元素所调用,并且可以在元素传递给算法之前将其转换为其他值。
- 视图:图可以用来转换或过滤底层范围的元素。视图可以组合在一起,形成所谓的操作管道,以应用于一个范围。
- 工厂∶范围工厂被用来构建一个按需生成值的视图。
可以使用迭代器对范围内的元素进行迭代,这些迭代器可以通过诸如ranges::begin()
,end()
,rbegin()
等迭代器进行检索. 范围库还支持ranges::empty()
,data()
,cdata()
和size()
,后者返回范围内的元素数量,但只有在常量时间内检索大小时才有效,否则,可以使用std::distance()
计算范围的begin迭代器和end迭代器之间的元素数目.所有这些访问器都不是成员函数,而是独立的自由函数,都需要一个范围作为参数.
1.基于范围的算法
vector data{33, 11, 22};
std::sort(begin(data), end(data));
ranges::sort(data);
投影
许多基于范围的算法都有一个所谓的投影参数,即一个回调函数,用于在将每个元素移交给算法之前对其进行转换。
class Person {
public:
Person(string first, string last)
: m_firstName(move(first)), m_lastName(move(last)) {}
const string &getFirstName() const { return m_firstName; }
const string &getLastName() const { return m_lastName; }
private:
string m_firstName{};
string m_lastName{};
};
vector persons{Person{"John", "White"}, Person{"Chris", "Blue"}};
ranges::sort(persons, {}, &Person::getFirstName);
ranges::sort(persons, {},
[](const Person &person) { return person.getFirstName(); });
注:需要同时包含<ranges>
和<algorithm>
头文件
2.视图
视图允许对基础范围的元素执行操作。视图可以被链接/组合在一起,形成一个管道,对一个范围的元素执行多个操作。组合视图很容易,只需要使用按位 OR(或)运算符、operator|
组合不同的操作。
属性:
- 惰性评估:仅仅构建一个视图还不能对底层范围执行任何操作。视图的操作仅在迭代视图的元素和解引用这样的迭代器时应用。
- 非占有:视图不拥有任何元素。顾名思义,它是一个可以存储在某个容器中的范围元素的视图,并且该容器是数据的所有者。视图只允许以不同的方式查看数据。因此,视图中元素的数量不会影响复制、移动或销毁视图的成本。
- 非变异:视图永远不会修改底层范围中的数据。
视图本身也是一个范围,但并非每个范围都是一个视图.容器是一个范围而不是一个视图,因为它拥有元素.
可以使用范围适配器创建视图,范围适配器接收一个范围(或者另一个视图)和一些可选参数,并返回一个新视图.
范围适配器 | 描述 |
---|---|
views::all | 创建一个包含范围内所有元素的视图 |
filter_view views::filter | 根据给定谓词过滤范围内的元素:如果谓词返回true ,则保留该元素,否则跳过该元素 |
transform_view views::transform | 对范围中的每个元素应用回调,以便将元素转换为其他值(可能是不同类型的值) |
take_view views_take | 创建范围的前N个元素的视图 |
take_while_view views::take_while | 创建范围的初始元素的视图,直到到达给定谓词返回 false 的元素为止 |
drop_view views::drop | 通过删除范围的前 N个元素创建视图 |
drop_while_view views::drop_while | 通过删除范围的所有初始元素创建视图,直到到达给定谓词返回 false 的元素 |
reverse_view views::reverse | 创建一个视图,该视图以相反的顺序迭代范围中的元素。这个范围必须是双向范围 |
elements_view views::elements | 需要一组类似元组的元素,创建一个类似元组元素的第N个元素的视图 |
keys_view views_keys | 需要一组类似对的元素,创建每个对的第1个元素的视图 |
values_view views::values | 需要一组类似对的元素,创建每个对的第2个元素的视图 |
common_view views_common | 根据范围的类型,begin() 和 end() 可能会返回不同的类型,例如 begin 迭代器和所谓的哨兵。这意味着,不能将这样的迭代器对传递给期望它们具有相同类型的函数。common_view 可用于将这样一个范围转换为一个公共范围,即 begin() 和 end() 返回相同类型的范围。 |
第1列中的范围适配器既显示了 std::ranges
名称空间中的类名,也显示了来自std::ranges::views
名称空间中的所谓范围适配器对象(range adapter object)。标准库还提供了一个名为std::views
的名称空间别名,它等于std::ranges::views
每个范围适配器都可以通过调用其构造函数并传递任何必需的参数来构造。第1个参数总是要操作的范围,后面跟着0个或多个附加参数:
std::ranges::operator_view {range, arguments...};
通常,不会使用它们的构造函数创建这些范围适配器,而是使用 std::ranges::views
名称空间中的
范围适配器对象,并结合按位 OR运算符|:
range | std::ranges::views::operator(arguments...);
void printRange(string_view message, auto &range) {
cout << message;
for (const auto &value : range) {
cout << value << " ";
}
cout << endl;
}
int main()
{
vector values{1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
printRange("original sequence:", values);
//filter out all odd values, leaving only the even values.
auto result{values |
views::filter([](const auto &value) { return value % 2 == 0; })};
printRange("only even value:", result);
//transform all values to their double value.
auto result2{result |
views::transform([](const auto &value) { return value * 2.0; })};
printRange("values double:", result2);
//drop the first 2 elements
auto result3{result2 | views::drop(2)};
printRange("first dropped:", result3);
//reverse the view
auto result4{result3 | views::reverse};
printRange("sequence reversed:", result4);
}
//output
original sequence:1 2 3 4 5 6 7 8 9 10
only even value:2 4 6 8 10
values double:4 8 12 16 20
first dropped:12 16 20
sequence reversed:20 16 12
注意:printRange()
函数实际上是一个函数模板(使用了auto
关键字),所以不能将声明与定义分离。
等效于:
auto result5{values |
views::filter([](const auto &value) { return value % 2 == 0; }) |
views::transform([](const auto &value) { return value * 2; }) |
views::drop(2) | views::reverse};
printRange("Final result:", result5);
通过视图修改元素
只要该范围不是只读的,就可以修改范围的元素。
views::transform
的结果就是一个只读视图。
vector values{1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
printRange("original sequence:", values);
//filter out all odd values, leaving only the even values.
auto result{values |
views::filter([](const auto &value) { return value % 2 == 0; })};
printRange("only even value:", result);
//drop the first 2 elements
auto result3{result | views::drop(2)};
printRange("first dropped:", result3);
//reverse the view
auto result4{result3 | views::reverse};
printRange("sequence reversed:", result4);
for (auto &value : result4) {
value *= 10;
}
printRange("Origin", values);
//output
original sequence:1 2 3 4 5 6 7 8 9 10
only even value:2 4 6 8 10
first dropped:6 8 10
sequence reversed:10 8 6
Origin1 2 3 4 5 60 7 80 9 100
映射元素
转换范围的元素并不需要产生具有相同类型元素的范围。相反,可以将元素映射到另一种类型。
主要是使用views::transform()
3.范围工厂
范围库提供了范围工厂(range factory)来构建视图,这些视图可以根据需要惰性地生成元素.
范围工厂 | 描述 |
---|---|
empty_view | 创建一个空视图 |
single_view | 创建具有单个给定元素的视图 |
iota_view | 创建一个无限或有界的视图,其中包含以初始值开始的元素,每个后续元素的值等于前一个元素的值加1 |
basic_istream_view istream_view | 创建一个视图,其中包含通过调用底层输入流上的调用提取运算符(运算符>> )检索到的 元素 |
输入流作为视图
basic_istream_view
/istream_view
范围工厂可用于构建从输入流读取元素的视图,使用运算符>>
读取元素.
for (auto value : ranges::istream_view<int>(cin) |
views::take_while([](const auto &v) { return v < 5; }) |
views::transform([](const auto &v) { return v * 2; })){
cout << format("> {}\n",value);
}