ranges标准库
ranges是对一系列数据的抽象,只有有头有尾,它既可以是一个有界容器,也可以是一个无穷列表.一旦有了这种抽象,就可以进一步构造range的适配器,它能够被管道操作符进行组合,以便对数据进行灵活的处理,而这些处理都是延迟计算的
用ranges编程生成小于1000的奇平方数:
auto res = std::views::iota(1) //生成从1开始的无穷序列
| std::views::transform([](auto n){return n * n;})
| std::views::filter([](auto n){return n % 2 == 1;})
| std::views::take_while([](auto n){return n < 1000;});
for(auto x : res){
std::cout << x << std::endl;
}
通过iota创建一个从1开始的无穷数列ranges,随后与各个适配器进行组合处理,transform对ranges中的每个数进行求平方操作,对平方的结果通过filter进行过滤,过滤条件为奇数,最终的奇数交给take_while处理,取所有小于1000的奇数
这种函数式编程风格有如下特点
- 延迟计算
- 灵活的组合能力
- 使程序通过消除循环的方式避免大量状态操作
range访问操作
C++标准库中的容器都可以视作一个range,普通数组和自定义类型也可以对range进行建模
标准库中提供了非成员函数去访问这些range
- std::ranges::begin
考虑对象r为一个range,函数调用std::ranges::begin®返回一个起始迭代器
- 若r为右值临时对象,则编译错误
- 若r为数组,则返回结果为r+0
- 若存在成员函数begin且满足要求则返回r.begin()
- 若r通过依赖查找(ADL)找到begin( r ),且返回类型满足要求,则结果为begin( r )
考虑如下代码
namespace ns{
struct Foo{};
void swap (Foo &a , Foo & b)noexcept {puts("Foo swap");}
};
ns::Foo a , b;
std::swap(a , b);
上述代码的调用结果总是使用标准库中更通用的std::swap版本,而不是自定义的交换函数,为避免这个问题,在泛型代码中,常常使用如下两步模式
using std::swap; //第一步,引入std::swap
swap(a , b); //第二步,未限定名称查找(ADL)
第一步,在当前作用域范围内引入std::swap;第二步,未限定名称查找。编译器会优先去对象所在名称空间下查找
而使用新一代ranges标准库里提供的算法,即便使用ADL也能实现上述效果
ranges::swap(a , b); //Foo swap
ranges标准库通过这种方式可以实现与传统标准算法库的精准隔离,即便使用两步模式编译器也会优先使用函数对象进行调用
using std::ranges::swap;
swap(a , b);
- std::ranges::end 用来返回ranges的终止迭代器
终止迭代器也叫哨兵迭代器
std::vector<int> v{3, 1, 4};
if (std::ranges::find(v, 5) != std::ranges::end(v))
std::cout << "在 vector v 中找到了 5!\n";
- std::ranges::size 用来返回一个range的大小
- 若为数组则返回数组的大小
- 否则,使用成员函数r.size()
- 否者,通过ADL的非泛型自由函数size( r )
- 否则,使用std::ranges::end( r )-std::ranges::begin( r )
auto il = {7}; // std::initializer_list
std::cout << "ranges::size(il) == " << std::ranges::size(il) << '\n';
int array[]{4, 5}; // array has a known bound
std::cout << "ranges::size(array) == " << std::ranges::size(array) << '\n';
- std::ranges::empty 判断一个range是否为空
- 使用bool(r.empty())
- 否则,使用std::ranges::size( r )是否为0
- 否则,通过std::ranges::begin( r )==std::ranges::end( r )判断
template <std::ranges::input_range R>
void print(char id, R&& r)
{
if (std::ranges::empty(r))
{
std::cout << '\t' << id << ") Empty\n";
return;
}
std::cout << '\t' << id << ") Elements:";
for (const auto& element : r)
std::cout << ' ' << element;
std::cout << '\n';
}
- std::ranges::data 获得一个rang的数据地址
- 若r为临时右值对象则编译错误
- 否则,使用成员函数r.data(),切返回为指针类型
- 否则,使用r.begin(),要求返回的迭代器对连续迭代器建模
#include <cstring>
#include <iostream>
#include <ranges>
#include <string>
int main()
{
std::string s{"Hello world!\n"};
char a[20]; // storage for a C-style string
std::strcpy(a, std::ranges::data(s));
// [data(s), data(s) + size(s)] is guaranteed to be an NTBS
std::cout << a;
}
ranges相关概念
- std::ranges::range
range概念定义了允许迭代其元素的类型,它要求一个可迭代的类型能够返回迭代元素的起始于哨兵迭代器,数组,标准库中的容器都可以视作range
template< class T >
concept range = requires( T& t ) {
ranges::begin(t); // equality-preserving for forward iterators
ranges::end (t);
};
- std::ranges::borrowed_range
引用要求引用的生命周期小于它所引用对象的生命周期,这样避免了悬挂引用
borrowed_range基于此概念,表达对range的借用(这里使用左值引用而不是右值,因为右值常常用来表达移动语义,会转移对象所有权)
template<std::ranges::borrowed_range R>
void f1(R && r){
std::cout << "concept borrowed_range" << std::endl;
return ;
}
f(vector<int>{1,2,3,4});//编译错误,一个右值
vector<int>vec{1,2,3,4};
f(vec);
string_view是C++17标准库引入的字符串类,它拥有和const char *一样小的传递开销
f(vector<int>{1,2,3,4});//编译错误,一个右值
f(string_view{"1234"});//编译成功,一个右值
这里编译成功的原因是string_view对变量模板enable_borrowed_range进行特化,使得它满足其概念要求
- std::ranges::sized_range
sized_range 概念指定在常数时间内以 size 函数知晓其大小的 range 类型
- std::ranges::view
它在range概念的基础上,要求能够在常量时间复杂度内进行移动构造,移动赋值与析构
enable_view 变量模板用于指示 range 是否为 view
根据定义,标准库中的容器都只是range而不是view但string_view是view
- 其他概念
rang 实用组件
- std::ranges::view_interface
用来创建自己的view
#include <ranges>
#include <vector>
#include <iostream>
template <class T, class A>
class VectorView : public std::ranges::view_interface<VectorView<T, A>> {
public:
VectorView() = default;
VectorView(const std::vector<T, A>& vec) :m_begin(vec.cbegin()), m_end(vec.cend()) {}
auto begin() const { return m_begin; }
auto end() const { return m_end; }
private:
typename std::vector<T, A>::const_iterator m_begin{}, m_end{};
};
int main()
{
std::vector<int> v = {1, 4, 9, 16};
VectorView view_over_v{v};
// 我们能以 begin() 与 end() 迭代。
for (int n : view_over_v) {
std::cout << n << ' ';
}
std::cout << '\n';
// 我们能在继承 view_interface 时免费地获得 operator[]
// 因为我们满足 random_access_range 概念。
for (std::ptrdiff_t i = 0; i < view_over_v.size(); i++) {
std::cout << "v[" << i << "] = " << view_over_v[i] << '\n';
}
}
// 1 4 9 16
// v[0] = 1
// v[1] = 4
// v[2] = 9
// v[3] = 16
range 工厂
- std::ranges::views::empty
无元素的空 view
#include <ranges>
int main()
{
std::ranges::empty_view<long> e;
static_assert(std::ranges::empty(e));
static_assert(0 == e.size());
static_assert(nullptr == e.data());
static_assert(nullptr == e.begin());
static_assert(nullptr == e.end());
}
- std::ranges::views::single
含有具有指定值的单个元素的 view
- std::ranges::views::iota
由通过重复对某个初值自增所生成的序列组成的 view
for (int i : std::ranges::iota_view{1, 10})
std::cout << i << ' ';
std::cout << '\n';
//1 2 3 4 5 6 7 8 9
- std::ranges::istream_view
由在关联的输入流上相继应用 operator>> 获得的元素组成的 view
auto v = std::ranges::istream_view<int>(std::cin);
for(auto e : v)std::cout<< e << " ";
range 适配器
- std::ranges::views::all
尝试把输入的range转化成view
std::vector<int> v{0, 1, 2, 3, 4, 5};
for (int n : std::views::all(v) | std::views::take(2))//转化并取前两个
std::cout << n << ' ';
- std::ranges::views::filter
filter接受一个rabge和一个谓词,并返回满足谓词的filter_view
#include <iostream>
#include <ranges>
int main()
{
auto even = [](int i){ return 0 == i % 2; };
auto square = [](int i){ return i * i; };
for (int i : std::views::iota(0, 6)
| std::views::filter(even)
| std::views::transform(square))
std::cout << i << ' ';
}
//0 4 16
- std::ranges::views::transform
transform适配器接受一个rang和一个计算函数,返回函数计算后的transform_view
- std::ranges::views::take 接受一个range和一个N,返回range的前N个元素
- std::ranges::views::take_while 接受一个range和一个谓词,它取所有元素直到不满足谓词
- std::ranges::views::drop 接受一个range和一个N,扔掉前N个元素并返回剩余元素
- std::ranges::views::drop_while 接受一个range和一个谓词,它扔掉所有元素直到不满足谓词
- std::ranges::views::join 表示由从拉平范围的视图获得的序列组成的 view。
- std::ranges::views::split split_view 接收一个 view 与一个分隔符,并按分隔符切割 view 为子范围。
- std::ranges::views::common 返回传统迭代器
auto r1 = std::ranges::subrange{i1, std::default_sentinel};
// auto e1 = std::accumulate(r1.begin(), r1.end(), 0); // 错误:要求“共同范围”
auto c1 = std::ranges::common_view{r1};
std::cout << "accumulate: " << std::accumulate(c1.begin(), c1.end(), 0) << '\n';
- std::ranges::views::reverse 代表有逆序的底层 view 的视图。
改善
ranges标准库改变了algorithm标准库