哨兵和结束迭代器有什么区别?

What's the difference between a sentinel and an end iterator?

本文关键字:什么 区别 迭代器 结束 哨兵      更新时间:2023-10-16

在阅读Eric Niebler的范围提案时,
我曾经遇到过用sentinel来代替end迭代器的情况。
我很难理解sentinel相对于end迭代器的好处。
谁能提供一个清晰的例子,说明sentinel带来了什么标准迭代器对无法实现的功能?

" 哨兵是过端迭代器的抽象。哨兵是可用于表示范围结束的常规类型。一个sentinel和表示范围的迭代器应为EqualityComparable。当迭代器i比较等于时,sentinel表示一个元素哨兵,我指向那个元素。"——

我认为哨兵的作用是确定范围的结束,而不仅仅是位置?

Sentinel只是允许end迭代器具有不同的类型。

在过末端迭代器上允许的操作是有限的,但这并不反映在它的类型中。使用*.end()迭代器是不行的,但是编译器会允许你。

哨兵除其他事项外,没有一元解参或++。它通常与最弱的迭代器一样受限制,但在编译时强制执行。

这是有回报的。通常,检测最终状态比找到它更容易。有了哨兵,==可以在编译时而不是运行时分派到"检测另一个参数是否超过了结束"。

结果是,一些过去比等效C代码慢的代码现在可以编译到C级别的速度,例如使用std::copy复制以空结尾的字符串。如果没有哨兵,您要么必须在复制之前扫描以找到结束,要么传递带有bool标志的迭代器,表示"我是结束哨兵"(或等价),并在==上进行检查。

在使用基于计数的范围时,还有其他类似的优点。此外,像zip ranges1这样的内容变得更容易表达(末尾zip哨兵可以保存两个源哨兵,如果两个哨兵都保存,则返回相等:zip迭代器要么只比较第一个迭代器,要么比较两个迭代器)。

另一种思考方式是,算法倾向于不使用迭代器概念的全部丰富性来传递参数作为结束迭代器的过去,并且迭代器在实践中的处理方式不同。哨兵意味着调用者可以利用这个事实,这反过来又让编译器更容易利用它。


1压缩范围是当你从2个或更多范围开始,并像拉链一样将它们"压缩"在一起时得到的。这个范围现在覆盖了各个范围元素的元组。推进zip迭代器会推进每个"包含的"迭代器,解引用和比较也是如此。

引入哨兵的主要动机是,有很多迭代器操作是被支持的,但对于结束迭代器end()来说通常是不需要的。例如,通过*end()对其解引用,通过++end()对其递增,等等,几乎没有任何意义(*)。

相比之下,end()的主要用途仅仅是将其与迭代器it进行比较,以指示it是否位于它所迭代的对象的末尾。而且,通常在编程中,不同的需求和不同的应用会产生新的类型。

range-v3库将这个观察结果转化为一个假设(通过一个概念实现):它为end()引入了一个新类型,并且只要求它与相应的迭代器相等可比较——但不需要通常的迭代器操作)。这种新型的end()被称为哨兵

这里的主要优点是获得了抽象和更好的关注点分离,在此基础上编译器可能能够执行更好的优化。在代码中,基本思想是这样的(这只是为了解释,与range-v3库无关):

struct my_iterator;    //some iterator
struct my_sentinel
{
     bool is_at_end(my_iterator it) const
     {
         //here implement the logic when the iterator is at the end
     }
};
auto operator==(my_iterator it, my_sentinel s)  //also for (my_sentinel s, my_iterator it)
{
    return s.is_at_end(it); 
}

看到抽象了吗?现在,您可以在is_at_end函数中实现任何检查,例如:

  • stop never (get a infinite range)
  • N递增后停止(以获得计数范围)
  • 在遇到时停止,即*it = ''(用于循环c -string)
  • 在12点时停止(用于吃午饭),依此类推。

此外,在性能方面,可以利用检查中的编译时信息(例如,将上面的N视为编译时参数)。在这种情况下,编译器可能能够更好地优化代码。


(*)请注意,这并不意味着通常不需要这种操作。例如,--end()在某些地方是有用的,参见这个问题。然而,似乎可以在没有这些的情况下实现标准库——这就是range-v3库所做的。

哨兵迭代器和end迭代器的相似之处在于它们标记范围的结束。它们的不同之处在于如何检测这一目的;要么测试迭代器本身,要么测试迭代器上的数据值。如果您已经在数据上执行测试,哨兵可以让您的算法"免费"完成,而无需任何额外的测试。这既可以简化代码,也可以使其更快。

一个非常常见的哨兵是0字节,用于标记字符串的结束。没有必要为字符串的末尾保留一个单独的迭代器,它可以在处理字符串本身的字符时确定。这种约定的缺点是字符串不能包含零字符。

请注意,我在阅读链接中的提案之前写了这个答案;这是哨兵的经典定义,可能与这里提出的定义不一致。