C++20范围的意义是什么?

3

我很难理解C++20范围相比于老式迭代器有何优势。是的,我猜不再需要使用beginend了,但是像这样的简单重载:

namespace std {
    template <typename Container>
    auto transform(Container&& container, auto&&... args) requires ( requires {container.begin(); container.end(); }) {
         return transform(container.begin(), container.end(), args...);
    }
}

什么是区间(ranges),在何时应该使用它们而不是迭代器?

编辑:我知道区间相较于迭代器有其他优势(链式调用,更好的方法等等...)。然而,这些(我想?)都可以通过迭代器实现,我不明白为什么需要引入一个全新的概念——区间。

将解决这个问题。


1
这是一个非常好的问题,我自己也在努力寻找答案。 - undefined
6
也可以使用迭代器完成,但不够方便。按照同样的逻辑,我们应该使用纯C编写代码,因为它可以做到C++能做到的任何事情,等等。 - undefined
1
@SomeProgrammer 不行,你不能这样做。因为一个单独的迭代器只有一半的信息。你无法链式操作迭代器。it | transform(f) 可以给你一个适配后的迭代器,但你还需要适配结束迭代器才能做任何事情。仅仅能做一些事情是不够的。想象一下使用 filter 会是什么样子 - undefined
2
@某程序员 哦,你是说你需要一个范围? - undefined
2
@SomeProgrammer: "我认为你可以重载管道操作符,使得迭代器能够像范围一样链式调用方法。" 如果是这样的话,请提供一些证据。在你的问题中加入一些代码,展示如何在普通迭代器上进行链式调用和其他范围操作。 - undefined
显示剩余10条评论
2个回答

10
你在这里反驳了自己的结论,证据如下:
template <typename Container>
auto transform(Container&& container, auto&&... args)
  requires ( requires {container.begin(); container.end(); }) {

所以...这是什么?这是一个接受满足约束条件的模板参数的函数。我们先不考虑这个约束要求成员begin/end而不是更合理的std::ranges::begin/end要求。
你打算将这个要求应用到多少个函数上?可能很多。每个算法都会有一个满足这个要求的版本。所以这看起来不像是一个一次性的要求,更像是应该被命名为concept的东西。
特别是因为这个概念可能需要指定算法所需的迭代器的类型。你不仅仅需要成员begin/end;你需要它们返回一个input_or_output_iterator和一个作为该迭代器的sentinel_for
requires ( requires(Container c)
{
  {c.begin()} -> input_or_output_iterator;
  {c.end()} -> sentinel_for<decltype(c.begin())>;
})

你真的想每次询问一个“容器”时都要输入这个吗?当然不是,这就是命名概念存在的意义。
那么这个概念是什么呢?它是一个可以迭代的东西,是通过特定的迭代器接口访问的一系列值。
而且这个概念的名称应该选择得不暗示对元素序列的所有权。无论给定的是序列的所有者还是其他,"container"都绝对是错误的名称。
所以让我们把这个概念称为“range”(范围)值序列。值序列可以通过迭代器/终止器接口进行迭代。而且你可能需要有不同类别的值序列。输入序列、前向序列、连续序列等等。你可能想要检测序列是否能在常数时间内计算大小,或者序列是否有界限或从其所有者借用。
如果你能编写操作符来创建这些值序列的视图,那不是很棒吗?
不管怎么说,一个范围换个名字也同样香甜。一旦你开始将迭代器与终止器配对,它将永远主宰你的命运。
处理迭代器时,范围是一个自然的概念。而所有基于范围概念构建的内容都是其发展的产物。

4

标准库核心概念的大多数目的,例如迭代器,是为了统一通常在标准库中使用的抽象。对于迭代器来说,这意味着提供一个接口,用于常用的概念:“这指向容器中的一个元素,并且我们希望能够遍历容器”。

而范围的目的则是将原始迭代器隐藏起来,不让它们出现在公共用户界面上。在迭代过程中,我们经常需要两个指针,即容器的开始和结束位置。范围试图通过隐藏此接口并提供单个接口,使所有函数都可以在begin()和end()之间操作,从而简化此过程。

特别是范围视图是使用范围的主要原因。当您想进行函数组合时,它们使代码更易于阅读。 cppreference 中的示例是一个很好的用例。

#include <ranges>
#include <iostream>
 
int main()
{
    auto const ints = {0,1,2,3,4,5};
    auto even = [](int i) { return 0 == i % 2; };
    auto square = [](int i) { return i * i; };
 
    // "pipe" syntax of composing the views:
    for (int i : ints | std::views::filter(even) | std::views::transform(square)) {
        std::cout << i << ' ';
    }
 
    std::cout << '\n';
 
    // a traditional "functional" composing syntax:
    for (int i : std::views::transform(std::views::filter(ints, even), square)) {
        std::cout << i << ' ';
    }
}

与原始迭代器相比,范围和视图提供了更高级的抽象层,几乎没有额外的成本。据我个人观点,在不使用C++范围时,特别是“视图管道”比代码更易读且更易于维护。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接