Non-Profit, International

Spirit unsterblich.

C++ Ranges

字数统计:1753 blog

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::splitwords 进行分割,然后使用 views::transform 把分割后的结果转换为 std::string_view,最后用 to 将结果存入 std::vector

为了学习 Ranges 的使用,需要理解一些最基础的概念:

整个 Ranges 库都围绕着迭代这一个概念进行设计,因此,首先要存在一个可迭代对象,这个迭代对象就是 range,一般译作范围,可以通过 std::range::range 这个 concept 进行判断。

标准库中所有容器,以及 std::spanstd::string_view 都是范围,此外,范围库中也有一些范围。范围可以有穷也可也无穷,只需要其能够进行迭代。

有了 range 之后,需要的第二样东西是 range adapter,一般译作范围适配器。范围适配器储存了一个行为,这个行为会在遍历的时候得到体现,范围适配器也是 range。虽然范围适配器才是最关键的,但一般使用的都是范围适配器对象,用管道运算符进行连接。范围适配器对象是范围适配器的生成器,之所以说是对象是因为范围适配器对象类似于 lambda,是一个重载了函数调用运算符的类的对象。

上面使用的 std::views::splitstd::views::transform 都是范围适配器对象,C++23 中一共有 22 种范围适配器,21 种范围适配器对象,通过组合适配器,可以实现对算法的灵活运用。

大部分范围适配器对象非常好懂,这里仅说明一个特殊点:

通常来讲一个 range 产生一个范围适配器后,该范围适配器的结果可以直接用来构造一个同类型的 range,但是 splitchunk 使得 range 维度提高,导致结果的元素实际上是由 subrange 储存的,这时就需要使用 transform 对结果的元素(元素也是 view)进行转换,就和文章开始的 string_viewsplit 一样。

范围适配器对象除了可以使用管道运算符进行使用,还可以用传统函数的方式使用:


#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_viewistream 生成一个来自 istream 的 view;repeat_viewrepeat 生成一个重复的 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';
}

参考:

若无特殊声明,本人原创文章以 CC BY-SA 3.0 许可协议 提供。