2

C++20 引入了ranges::borrowed_range,它定义了一个范围的要求,这样一个函数可以按值获取它并返回从它获得的迭代器,而不会有悬空的危险。简而言之(参考P2017R1):

当范围超出范围后您可以保留其迭代器时,范围就是一个借用范围。

同时,borrowed_subrange_t还引入了一个类型助手:

template<ranges::range R>
using borrowed_subrange_t = std::conditional_t<
    ranges::borrowed_range<R>,
    ranges::subrange<ranges::iterator_t<R>>, 
    ranges::dangling
>;

这是一个别名模板,由一些受约束的算法使用,例如ranges::uniqueranges::find_end避免返回潜在的悬空迭代器或视图。

当 type Rmodelsborrowed_range时,borrowed_subrange_tofR基本上是 a subrange<ranges::iterator_t<R>>,这意味着它也是 a ranges::common_range,因为它只接受一个模板参数,而第二个参数默认与第一个参数相同。

但是似乎有些误导,因为有些subrange类型可以借用但仍然不是common_range,请考虑以下代码:

auto r = views::iota(0);
auto s1 = ranges::subrange{r.begin(),     r.begin() + 5};
auto s2 = ranges::subrange{r.begin() + 5, r.end()};

subrange从 a 创建了两个 s borrowed_range ranges::iota_view,一个包含前 5 个元素,另一个包含itoa_view从第五个元素开始的所有元素。它们是subranges 的itoa_view,而且它们显然是借来的:

static_assert(ranges::borrowed_range<decltype(s1)>);
static_assert(ranges::borrowed_range<decltype(s2)>);

所以在某种程度上,它们的类型都可以看成是borrowed_subrange_t类型的itoa_view,但是根据定义,只有类型s1borrowed_subrange_t类型r,这也意味着下面的代码是非良构的,因为iota_view r它不是一个common_range

auto bsr = ranges::borrowed_subrange_t<decltype(r)>{r}; // ill-formed

为什么标准需要保证borrowed_subrange_tof some range Ris a common_range,即begin()and的返回类型end()相同?这背后的原因是什么?为什么不更一般地定义它:

template <ranges::range R>
using borrowed_subrange_t = std::conditional_t<
    ranges::borrowed_range<R>,
    ranges::subrange<
      ranges::iterator_t<R>, 
      std::common_iterator<
        ranges::iterator_t<R>,
        ranges::sentinel_t<R>
      >
    >,
    ranges::dangling
>;

这样做会不会有任何潜在的缺陷和危险?

4

2 回答 2

3

引用 Alexander Stepanov 在“从数学到通用编程”中的话:

编写代码时,通常会计算出调用函数当前不需要的值。但是,稍后,当在不同情况下调用代码时,此值可能很重要。在这种情况下,你应该遵守有用回报法则:一个过程应该返回它计算的所有潜在有用的信息。

borrowed_subrange用于必须遍历整个子范围的算法。所以我们必须计算这个范围的结束迭代器作为执行算法其余部分的副作用。这对用户很有用,所以我们应该返回它!

对于其中一些算法,实际上甚至不可能返回哨兵。例如,ranges::search必须返回一个匹配的子范围 - 但该子范围不必位于初始范围的最末端,因此返回原始哨兵根本不是一种选择。

对于其他算法,返回哨兵可能是一种选择,但这是一个糟糕的选择。考虑unique。这里基本上有三种选择:

  1. I仅返回表示此范围开始的迭代器 ( )(与此相同std::unique
  2. 返回subrange<I, S>表示整个范围(即仅通过提供的last
  3. 返回subrange<I>表示整个范围,包括计算的I引用last

但是我们已经在做能够做到(3)的工作,所以这更有价值。没有理由这样做(2)。


考虑一个不太抽象的情况,我们实际上有一个哨兵。假设我们有一个以 null 结尾的字符串:

struct null_terminated_string {
    char const* p;

    struct sentinel {
        auto operator==(char const* p) const { return *p == '\0'; }
    };

    auto begin() const -> char const* { return p; }
    auto end() const -> sentinel { return {}; }
};

现在,从 : 中返回的更有用的返回是unique一个,它只返回这种null_terminated_string::sentinel类型,还是返回char const*指向空终止符的 a?后者为您提供了更多有用的信息(包括,例如,大小!)。


最后,这个:

template <ranges::range R>
using borrowed_subrange_t = std::conditional_t<
    ranges::borrowed_range<R>,
    ranges::subrange<
      ranges::iterator_t<R>, 
      std::common_iterator<
        ranges::iterator_t<R>,
        ranges::sentinel_t<R>
      >
    >,
    ranges::dangling
>;

没有意义,因为common_iterator<iterator_t<R>, sentinel_t<R>>它不是iterator_t<R>. 会是这样的:

template <ranges::range R>
using borrowed_subrange_t = std::conditional_t<
    ranges::borrowed_range<R>,
    ranges::subrange<ranges::iterator_t<R>, ranges::sentinel_t<R>>,
    ranges::dangling
>;

可能是有道理的。考虑ranges::find。现在,它只是返回一个iterator_t<R>(或者,更准确地说,是一个iterator_t<R>dangling)。但是不同的设计ranges::find可能会做一些不同的事情:它可以从该迭代器开始返回一个子范围,并包括范围的整个其余部分(可以说这会更有用)。如果我们想这样做ranges::find,我们肯定会想要返回一个subrange<iterator_t<R>, sentinel_t<R>>. 在这种情况下,我们还没有遍历整个范围,我们不想为此付出额外的成本;我们只需通过哨兵转发。

只是没有任何看起来像这样的算法,<algorithm>那些只是简单地将迭代器而不是子范围返回到末尾。如果我们有这样的算法,我们肯定会有一个borrowed_subrangeused的版本sentinel_t<R>。但是有了我们现有的算法,就不需要这样的东西了。

于 2021-03-26T15:34:47.603 回答
2

为什么标准需要保证某个范围R的borrowed_subrange_t是common_range,即begin()和end()的返回类型相同?

并非所有子范围都以基础范围的标记值结束。

这样做会不会有任何潜在的缺陷和危险?

如果基础范围的标记为空类型,则所有子范围都将在标记处结束,而不是在所需的结尾处。

于 2021-03-26T13:14:09.223 回答