范围:C++ 代码的优雅进化,告别迭代器,拥抱更简洁的未来!

一、简介

C++ 标准模板库 (STL) 是一个非常棒的工具,可以使代码更正确、更具表现力。它主要由两部分组成:

  • 容器: 例如 std::vectorstd::map
  • 算法: 一组相当庞大的通用函数,它们可以在容器上操作。这些算法主要位于 algorithm 头文件中。

许多使用 for 循环对容器进行的手动操作都可以用调用 STL 算法来代替。这样做可以使代码更清晰,因为无需费力解析复杂的 for 循环,就能立即理解代码的含义,因为那些令人头疼的 for 循环已经被明确的名称所取代,例如 std::copystd::partitionstd::rotate

但是,STL 仍然存在一些可以改进的地方。这篇文章将重点关注其中的两个方面:

  • 迭代器: 所有算法都操作指向它们所操作的集合的迭代器。虽然这在某些特定情况下很有用,比如在容器中的某个特定点停止,但大多数情况下,需要遍历整个容器,从它的 .begin().end()

    std::copy(v1.begin(), v1.end(), std::back_inserter(v2));
    std::set_difference(v2.begin(), v2.end(), v3.begin(), v3.end(), std::back_inserter(v4));
    std::transform(v3.begin(), v3.end(), std::back_inserter(v4));
    

    (注意:上面使用的 std::back_inserter 是一个输出迭代器,它每次被赋值时都会在它所传递的容器中执行 push_back 操作。这免去了程序员对输出容器进行大小调整的麻烦。)

  • 算法组合性: 使用 STL 的 C++ 开发人员经常需要对满足某个谓词的集合元素应用某个函数。对集合 input 中的所有元素应用函数 f 并将结果放入向量 output 中,可以使用 std::transform 实现:

    std::transform(input.begin(), input.end(), std::back_inserter(output), f);
    

    而根据谓词 p 过滤元素可以使用 std::copy_if 实现:

    std::copy_if(input.begin(), input.end(), std::back_inserter(output), p);
    

    但是,没有简单的方法可以将这两个调用组合起来,而且没有像“transform_if”这样的算法。

范围 提供了一种不同的 STL 使用方法,可以以非常优雅的方式解决这两个问题。范围最初是在 Boost 中引入的,现在正在走向标准化。

二、范围的概念

这一切的核心是 范围 的概念。本质上,范围是可以遍历的东西。更准确地说,范围是具有 begin()end() 方法的东西,这些方法返回对象(迭代器),这些对象允许你遍历范围(即,沿着范围的元素移动,并解引用以访问这些元素)。

用伪代码表示,范围应该符合以下接口:

Range
{
    begin()
    end()
}

这代表着所有 STL 容器本身都是范围。

在范围概念被定义之前,使用 STL 的代码已经以某种方式使用了范围,但很笨拙。正如这篇文章开头所见,它们是直接用两个迭代器(通常是 beginend)来操作的。然而,使用范围,通常不会看到迭代器。它们仍然存在,但被范围的概念抽象掉了。

这一点很重要。迭代器是技术性的构造,允许遍历集合,但它们通常对功能代码来说过于技术性。大多数情况下,真正想表示的是范围,它更符合代码的抽象级别。就像现金流范围、屏幕上的行范围或来自数据库的条目范围。

因此,以范围为单位进行编码是一个巨大的进步,因为从这个意义上说,迭代器违反了尊重抽象级别的原则,这是设计良好代码最重要的原则。

在范围库中,STL 算法被重新定义为直接接受范围作为参数,而不是两个迭代器,例如:

ranges::transform(input, std::back_inserter(output), f);

与之相对的是:

std::transform(input.begin(), input.end(), std::back_inserter(output), f);

这些算法在实现中重用了 STL 版本,通过将范围的 beginend 传递给原生 STL 版本。

三、智能迭代器

尽管范围将它们抽象掉了,但范围遍历仍然是通过迭代器实现的。范围的全部威力来自于它与 智能迭代器 的结合。一般来说,集合的迭代器有两个职责:

  • 沿着集合的元素移动(++-- 等)
  • 访问集合的元素(*->

例如,向量迭代器就是这样做的。但是,起源于 boost 的“智能”迭代器会自定义其中一个或两个行为。例如:

  • transform_iterator 使用另一个迭代器 it 和一个函数(或函数对象)f 构造,并自定义它访问元素的方式:当解引用时,transform_iteratorf 应用于 *it 并返回结果。
  • filter_iterator 使用另一个迭代器 it 和一个谓词 p 构造。它自定义了它的移动方式:当 filter_iterator 前进一个位置(++)时,它会前进其底层迭代器 it,直到它到达满足谓词的元素或集合的末尾。

四、结合范围和智能迭代器:范围适配器

范围的全部威力来自于它们与智能迭代器的结合。这是通过 范围适配器 实现的。

范围适配器是一个可以与范围结合以生成新范围的对象。它们的一部分是 视图适配器:使用它们,初始的适配范围保持不变,而生成的范围不包含元素,因为它只是对初始范围的视图,但具有自定义的迭代行为。

为了说明这一点,以 view::transform 适配器为例。这个适配器用一个函数初始化,可以与范围结合以生成一个对它的视图,该视图具有在该范围内使用 transform_iterator 的迭代行为。范围适配器可以使用 | 运算符与范围结合,这使得它们具有优雅的语法。

对于以下数字集合:

std::vector numbers = { 1, 2, 3, 4, 5 };

范围

auto range = numbers | view::transform(multiplyBy2);

是对向量 numbers 的一个视图,它具有使用函数 multiplyBy2transform_iterator 的迭代行为。因此,当遍历这个视图时,得到的结果都是这些数字,乘以 2。例如:

ranges::accumulate(numbers | view::transform(multiplyBy2), 0);

返回 1*2 + 2*2 + 3*2 + 4*2 + 5*2 = 30(类似于 std::accumulateranges::accumulate 对它所传递的范围的元素进行求和)。

还有很多其他的范围适配器。例如,view::filter 接受一个谓词,可以与范围结合以构建一个对它的视图,该视图具有 filter_iterator 的行为:

ranges::accumulate(numbers | view::filter(isEven), 0);

返回 2 + 4 = 6

需要注意的是,由与范围适配器关联而产生的范围,尽管它们只是对它们所适配的范围的视图,并且实际上不存储元素,但它们符合范围接口(beginend),因此它们本身就是范围。因此,适配器可以适配适配范围,并且可以有效地以以下方式组合:

ranges::accumulate(numbers | view::filter(isEven) | view::transform(multiplyBy2), 0);

返回 2*2 + 4*2 = 12。这为最初无法组合算法的问题提供了解决方案。

五、总结

范围提高了使用 STL 的代码的抽象级别,因此清除了使用 STL 的代码中多余的迭代器。范围适配器是一个非常强大且具有表现力的工具,可以以模块化的方式对集合的元素应用操作。

范围是 STL 的未来。要了解更多信息,可以查看 Boost 中的初始范围库
在这里插入图片描述

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Lion 莱恩呀

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值