C++ Ranges
C++20 的一个重要改进就是范围(Ranges)以及范围库,范围通过管道运算使得以往需要使用多重循环的算法可以顺序描述,大大提高了算法的可读性。
由于范围及范围库是一个非常重大的改进,因此这篇文章尽量从简。
首先给出一个使用范围进行字符串切割的代码:
#include <iostream>
#include <ranges>
#include <string_view>
#include <vector>
#include <string>
int main() {
using namespace std::literals;
constexpr auto words{ "Hello_C++_23_!"sv };
constexpr auto delim{ "_"sv };
auto result1 = words | std::views::split('_') | std::views::transform([](auto&& t) { return std::string_view{ t }; }) | std::ranges::to<std::vector>();
for (auto& i : result1) {
std::cout << i << ' ';
}
std::cout << std::endl;
// split 为 std::string,并且使用 delim 字符串做分割
auto result2 = words | std::views::split(delim) | std::views::transform([](auto t) { return std::string{ std::string_view{ t } }; }) | std::ranges::to<std::vector>();
for (auto& i : result2) {
std::cout << i << ' ';
}
std::cout << std::endl;
}
可以看到整个过程是非常清晰的,首先使用 ranges::split
对 words
进行分割,然后使用 views::transform
把分割后的结果转换为 std::string_view
,最后用 to 将结果存入 std::vector
。
为了学习 Ranges 的使用,需要理解一些最基础的概念:
整个 Ranges 库都围绕着迭代这一个概念进行设计,因此,首先要存在一个可迭代对象,这个迭代对象就是 range,一般译作范围,可以通过 std::range::range
这个 concept 进行判断。
标准库中所有容器,以及 std::span
,std::string_view
都是范围,此外,范围库中也有一些范围。范围可以有穷也可也无穷,只需要其能够进行迭代。
有了 range 之后,需要的第二样东西是 range adapter,一般译作范围适配器。范围适配器储存了一个行为,这个行为会在遍历的时候得到体现,范围适配器也是 range。虽然范围适配器才是最关键的,但一般使用的都是范围适配器对象,用管道运算符进行连接。范围适配器对象是范围适配器的生成器,之所以说是对象是因为范围适配器对象类似于 lambda,是一个重载了函数调用运算符的类的对象。
上面使用的 std::views::split
,std::views::transform
都是范围适配器对象,C++23 中一共有 22 种范围适配器,21 种范围适配器对象,通过组合适配器,可以实现对算法的灵活运用。
大部分范围适配器对象非常好懂,这里仅说明一个特殊点:
通常来讲一个 range 产生一个范围适配器后,该范围适配器的结果可以直接用来构造一个同类型的 range,但是 split
和 chunk
使得 range 维度提高,导致结果的元素实际上是由 subrange
储存的,这时就需要使用 transform
对结果的元素(元素也是 view)进行转换,就和文章开始的 string_view
的 split
一样。
范围适配器对象除了可以使用管道运算符进行使用,还可以用传统函数的方式使用:
#include <iostream>
#include <ranges>
#include <vector>
int main()
{
std::vector ints{ 1,2,3,4,5,6 };
auto even = [](auto i) {return (i & 1) == 0; };
auto square = [](auto i) {return i * i; };
for (auto i : ints | std::views::filter(even) | std::views::transform(square)) {
std::cout << i << ' ';
}
std::cout << std::endl;
// 等价的函数调用形式
for (auto i : std::views::transform(std::views::filter(ints, even), square)) {
std::cout << i << ' ';
}
std::cout << std::endl;
}
可以看出,实际上是范围适配器储存了每一层的结果,从左到右实际上是从内到外。在对范围适配器进行遍历的时候,才真正施加范围适配器上的效果,这个性质也被叫做惰性求值。
第三类要了解的东西是范围工厂。范围适配器并不储存任何对象,也不储存任何序列,而范围工厂用于生成一些具体的范围。C++23 中有 6 种范围工厂:
empty
是不存储任何元素的适配器的对象,由于其不储存任何元素,所以只需要声明为一个变量模板即可:
#include <ranges>
int main()
{
std::ranges::empty_view<long> e;
auto e1 = std::views::empty<long>;
static_assert(std::ranges::empty(e));
static_assert(0 == e.size());
}
single_view
以及 single
生成一个只有一个元素的 view;iota_view
以及 iota
生成一个自增的序列,注意 iota
不要求参数为整数类型,只需要其实现自增即可;istream_view
及 istream
生成一个来自 istream
的 view;repeat_view
及 repeat
生成一个重复的 view,和 iota_view
类似,repeat_view
可以是一个无界的 view,可以通过 take
来实现终止。此外还有一个生成笛卡儿积的工厂 cartesian_product
,由于不常用不做过多演示。
#include <iostream>
#include <iomanip>
#include <ranges>
#include <string>
#include <string_view>
#include <sstream>
int main()
{
using namespace std::literals;
constexpr std::ranges::single_view sv1{ 3.1415 };
static_assert(sv1);
static_assert(sv1.size() == 1);
constexpr std::ranges::iota_view iv1{ 1, 10 };
static_assert(iv1.size() == 9);
constexpr auto iv2 = std::views::iota(1);
for (int i : iv1)
std::cout << i << ' ';
std::cout << '\n';
for (int i : iv2 | std::views::take(9))
std::cout << i << ' ';
std::cout << '\n';
auto floats = std::istringstream{ "1.1 2.2\t3.3\v4.4\f55\n66\r7.7 8.8" };
std::ranges::copy(
std::ranges::istream_view<float>(floats),
std::ostream_iterator<float>{std::cout, ", "});
std::cout << '\n';
for (auto s : std::views::repeat("C++"sv, 4)) {
std::cout << s << ' ';
}
std::cout << '\n';
for (auto s : std::views::repeat("C++"sv) | std::views::take(4)) {
std::cout << s << ' ';
}
std::cout << '\n';
}