康桓瑋*_*康桓瑋 3 c++ range-v3 c++20 std-ranges
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
>;
Run Code Online (Sandbox Code Playgroud)
这是一个别名模板,由一些受限算法(例如ranges::unique和 )使用ranges::find_end,以避免返回潜在的悬空迭代器或视图。
当类型为Rmodels时borrowed_range,borrowed_subrange_tofR基本上是 a subrange<ranges::iterator_t<R>>,这意味着它也是 a ranges::common_range,因为它只接受一个模板参数,并且第二个默认与第一个参数的类型相同。
但似乎存在一些误导,因为有些subrange类型可以借用但仍然是 not 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()};
Run Code Online (Sandbox Code Playgroud)
我subrange从 a 创建两个 s borrowed_range ranges::iota_view,一个包含前 5 个元素,另一个包含itoa_view从第五个元素开始的所有元素。它们是subrange的itoa_view,并且显然是借用的:
static_assert(ranges::borrowed_range<decltype(s1)>);
static_assert(ranges::borrowed_range<decltype(s2)>);
Run Code Online (Sandbox Code Playgroud)
所以在某种程度上,它们的类型都可以被视为类型的borrowed_subrange_t,itoa_view但是根据定义,只有 的类型s1是borrowed_subrange_t类型的r,这也意味着下面的代码是格式错误的,因为iota_view r不是 an common_range:
auto bsr = ranges::borrowed_subrange_t<decltype(r)>{r}; // ill-formed
Run Code Online (Sandbox Code Playgroud)
为什么标准需要保证borrowed_subrange_tsomerange R是 a ,即 和的common_range返回类型相同?这背后的原因是什么?为什么不更一般地定义它,例如:begin()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
>;
Run Code Online (Sandbox Code Playgroud)
这样做会不会有潜在的缺陷和危险?
引用 Alexander Stepanov 在《从数学到通用编程》中的一句话:
\n\n\n编写代码时,通常会出现这样的情况:您最终计算出调用函数当前不需要的值。然而,稍后,当在不同情况下调用代码时,该值可能很重要。在这种情况下,您应该遵守有用返回定律:过程应该返回它计算出的所有可能有用的信息。
\n
borrowed_subrange用于必须遍历整个子范围的算法。因此,我们必须计算该范围的结束迭代器,作为执行算法其余部分的副作用。这对用户有用,所以我们应该返回它!
对于其中一些算法,实际上甚至不可能返回哨兵。例如,ranges::search必须返回匹配的子范围 - 但该子范围不必位于初始范围的最末尾,因此返回原始哨兵根本不是一个选项。
对于其他算法,返回哨兵可能是一种选择,但这是一个糟糕的选择。考虑unique。这里基本上有三种选择:
I仅返回表示此范围开始的迭代器 ( ) (如前所述std::unique)subrange<I, S>表示完整范围(即仅通过提供的last)subrange<I>表示完整范围,包括计算的I引用last。但我们已经在做能够做到 (3) 的工作,因此这更有价值。没有理由这样做(2)。
\n考虑一个不太抽象的情况,我们实际上有一个哨兵。假设我们有一个以 null 结尾的字符串:
\nstruct null_terminated_string {\n char const* p;\n\n struct sentinel {\n auto operator==(char const* p) const { return *p == \'\\0\'; }\n };\n\n auto begin() const -> char const* { return p; }\n auto end() const -> sentinel { return {}; }\n};\nRun Code Online (Sandbox Code Playgroud)\n现在,什么是更有用的 return from unique:一个只返回此null_terminated_string::sentinel类型,还是一个返回char const*指向空终止符的 a ?后者为您提供了更有用的信息(例如,包括尺寸!)。
最后,这个:
\ntemplate <ranges::range R>\nusing borrowed_subrange_t = std::conditional_t<\n ranges::borrowed_range<R>,\n ranges::subrange<\n ranges::iterator_t<R>, \n std::common_iterator<\n ranges::iterator_t<R>,\n ranges::sentinel_t<R>\n >\n >,\n ranges::dangling\n>;\nRun Code Online (Sandbox Code Playgroud)\n没有意义,因为common_iterator<iterator_t<R>, sentinel_t<R>>不是 的哨兵iterator_t<R>。会是这样的:
template <ranges::range R>\nusing borrowed_subrange_t = std::conditional_t<\n ranges::borrowed_range<R>,\n ranges::subrange<ranges::iterator_t<R>, ranges::sentinel_t<R>>,\n ranges::dangling\n>;\nRun Code Online (Sandbox Code Playgroud)\n这可能是有道理的。考虑ranges::find。现在,它只是返回一个iterator_t<R>(或者,更准确地说,返回一个iterator_t<R>或dangling)。但是不同的设计ranges::find可以做不同的事情:它可以返回从该迭代器开始的子范围,并包括该范围的整个其余部分(可以说这会更有用)。如果我们想这样做ranges::find,我们肯定会想返回 a subrange<iterator_t<R>, sentinel_t<R>>。在这种情况下,我们还没有遍历整个范围,并且我们不想为此付出额外的成本;我们只需通过哨兵转发即可。
只是在 中没有任何像这样的算法<algorithm>,这些算法只是简单地返回迭代器而不是子范围到末尾。如果我们有这样的算法,我们肯定会有一个borrowed_subrange使用的版本sentinel_t<R>。但根据我们现有的算法,不需要这样的事情。
| 归档时间: |
|
| 查看次数: |
578 次 |
| 最近记录: |