问题描述
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::unique
和 ranges::find_end
)使用它来避免返回潜在的悬空迭代器或视图。
当类型 R
模型 borrowed_range
时,borrowed_subrange_t
的 R
基本上是一个 subrange<ranges::iterator_t<R>>
,
这意味着它也是一个 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
ranges::iota_view
创建了两个 borrowed_range
,一个包含前 5 个元素,另一个包含 itoa_view
从第五个元素开始的所有元素.它们是 subrange
的 itoa_view
,显然是借来的:
static_assert(ranges::borrowed_range<decltype(s1)>);
static_assert(ranges::borrowed_range<decltype(s2)>);
所以在某种程度上,它们的类型都可以看作是borrowed_subrange_t
类型的itoa_view
,但是根据定义,只有s1
的类型是{{1 borrowed_subrange_t
类型的 }},这也意味着以下代码格式错误,因为 r
iota_view
不是 r
:
common_range
为什么标准需要保证一些auto bsr = ranges::borrowed_subrange_t<decltype(r)>{r}; // ill-formed
的{{1}}是borrowed_subrange_t
,即range
和{的返回类型{1}} 一样吗?这背后的原因是什么?为什么不更一般地定义它:
R
这样做会不会有任何潜在的缺陷和危险?
解决方法
引用 Alexander Stepanov 在“从数学到泛型编程”中的话:
在编写代码时,通常情况下您最终会计算出调用函数当前不需要的值。但是,稍后在不同情况下调用代码时,此值可能很重要。在这种情况下,你应该遵守有用返回定律:一个过程应该返回它计算出的所有可能有用的信息。
borrowed_subrange
用于必然遍历整个子范围的算法。所以我们必须计算这个范围的结束迭代器作为执行算法其余部分的副作用。这对用户很有用,所以我们应该返回它!
对于其中一些算法,实际上甚至不可能返回哨兵。例如,ranges::search
必须返回匹配的子范围 - 但该子范围不必位于初始范围的最末端,因此返回原始标记根本不是一种选择。
对于其他算法,返回哨兵可能是一种选择,但这是一个糟糕的选择。考虑unique
。这里基本上有三个选择:
- 仅返回表示此范围开始的迭代器 (
I
)(如std::unique
所做的那样) - 返回
subrange<I,S>
表示完整范围(即仅通过提供的last
) - 返回
subrange<I>
表示完整范围,包括计算出的I
引用last
。
但我们已经在做能够做 (3) 的工作,所以这更有价值。没有理由做(2)。
考虑一个不那么抽象的情况,我们实际上有一个哨兵。假设我们有一个以空字符结尾的字符串:
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*
类型的返回值?后者为您提供了更多有用的信息(例如,包括尺寸!)。
最后,这个:
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_subrange
的 sentinel_t<R>
版本。但是根据我们现有的算法,不需要这样的东西。
为什么标准需要保证某个范围R的borrowed_subrange_t是common_range,即begin()和end()的返回类型相同?
并非所有子范围都以基础范围的标记值结束。
这样做会不会有任何潜在的缺陷和危险?
如果基础范围的类型为空类型作为标记,则所有子范围都将在标记处结束,而不是在它们想要的结束处。