标准库容器如何知道过去结束迭代器之前的内容?

How do standard library containers know what's before the past-the-end iterator?

提问人:edugomez102 提问时间:7/7/2023 最后编辑:François Andrieuxedugomez102 更新时间:7/7/2023 访问量:109

问:

我正在尝试实现我自己的 和 ,并且我一直在使用该方法时遇到麻烦,因为这些容器在内存中不连续。listmapend()

经过一番研究,我最终实现了 as 方法,该方法返回一个拥有 .(end() 以何种方式指向非连续容器中的“one past the end”?这对于我的大部分实现来说已经足够了,因为它验证了当列表为空时(空列表没有节点,因此指向)。end()nullptrlist::begin() == list::end()headnullptr

但是,我已经看到可以调用过去的结束迭代器来指向容器的实际最后一个元素。operator--()

int main() {
  {
    std::map<int, char> m = {{1, 'a'}, {2, 'b'}, {3, 'c'}};
    auto it = m.end();
    --it;
    std::cout << it->first << "\n";  // output: 3
    std::cout << it->second << "\n"; // output: c
  }
  {
    std::list<int> l{ 1, 2, 8};
    auto it = l.end();
    --it;
    std::cout << *it << "\n"; // output: 8
  }
}

这是如何实现的?在 gcc 中实现 of 会更改当前节点以指向前一个节点:operator--()_List_iterator

      _Self&
      operator--() _GLIBCXX_NOEXCEPT
      {
    _M_node = _M_node->_M_prev;
    return *this;
      }

但是,如果节点不在列表中,这怎么可能呢?这个节点是如何存在的?这是否意味着分配了一个额外的节点?你真的可以调用过去的迭代器吗?operator--()

这些是 gcc 的 和 方法 ,以防万一:begin()end()std::list

     /**
       *  Returns a read-only (constant) iterator that points to the
       *  first element in the %list.  Iteration is done in ordinary
       *  element order.
       */
      _GLIBCXX_NODISCARD
      const_iterator
      begin() const _GLIBCXX_NOEXCEPT
      { return const_iterator(this->_M_impl._M_node._M_next); }

      /**
       *  Returns a read/write iterator that points one past the last
       *  element in the %list.  Iteration is done in ordinary element
       *  order.
       */
      _GLIBCXX_NODISCARD
      iterator
      end() _GLIBCXX_NOEXCEPT
      { return iterator(&this->_M_impl._M_node); }
C++ 迭代 器容器 std c++-standard-library

评论

8赞 bolov 7/7/2023
没有什么能阻止实现创建一个额外的节点(该节点链接到最后一个节点 as )并让迭代器指向它。作为用户,您不能访问该节点,只能访问迭代器,但实现可以。prev
5赞 Pepijn Kramer 7/7/2023
这可以通过添加所谓的前哨节点来完成。但是,您也不应该将迭代器视为指针,它们不是(至少并非总是如此),它们本身可以是对象,因此它们可以具有状态,表明它们不引用容器内的任何内容。
1赞 Ted Lyngmo 7/7/2023
struct NodeBase { NodeBase *prev, *next; } sentinel; struct Node : NodeBase { T data; }可能有用。
0赞 273K 7/7/2023
end() 作为返回保存 nullptr 的迭代器的方法。不,它不成立.GNU STL 返回引用最后一个列表项的迭代器。nullptrlistend()
2赞 273K 7/7/2023
无论如何,GNU 列表是一个循环低谷和 .__first <-> ... <-> __last <-> end() <-> __first_M_next_M_prev

答:

1赞 eerorika 7/7/2023 #1

但是,如果节点不在列表中,这怎么可能呢?

简单地说,指向最后一个节点。_M_node->_M_prev

这个节点是如何存在的?

与任何节点的存在方式相同。对于标准列表,它将使用提供的分配器创建。

这是否意味着分配了一个额外的节点?

这似乎是一个合理的结论。这种模式称为“前哨节点”。

你真的可以调用运算符--()到一个过去的迭代器吗?

是的。

1赞 robthebloke 7/7/2023 #2

标准容器使用了一些肮脏的伎俩(或聪明,取决于你看待它的方式)。(在一定程度上简化代码)。

从“基本”节点类型开始。这只是您的前进和后退指针。

struct ListNodeBase {
  ListNodeBase* next;
  ListNodeBase* prev;
};

列表节点使用要存储的实际数据扩展基本节点类型。指针和数据之间的这种分离有点有用!

template<typename T>
struct ListNode : public ListNodeBase {
  T data;
};

跨链表的迭代仅使用 ListNodeBase。为了访问数据,对象将上移到 ListNode。

template<typename T>
struct ListIter {
   ListNodeBase* current;

   // Here however we upcast to the node type to access the data. 
   T& operator*() const
      { return static_cast< ListNode<T>* >(current)->data; }
   T* operator->() const
      { return &static_cast< ListNode<T>* >(current)->data; }

   // just your bog standard list traversal.. 
   // again, this only operates on the 
   ListIter<T>& operator++() {
      current = current->next;
      return *this;
   }

   ListIter<T>& operator--() {
      current = current->prev;
      return *this;
   }
};

不过,诀窍是有一个 ListNodeBase(即列表容器)的节点,其余的节点是 ListNode。

template<typename T> 
struct list : private ListNodeBase {

   typedef ListIter<T> iterator;


   list() 
   {
     // when empty, store pointers to itself 
     this->next = this;
     this->prev = this;
   }

   iterator begin() 
     { return (iterator){ this->next }; }

   iterator end() 
     { return (iterator){ this }; }
};

换句话说,列表容器是哨兵节点(它只是不存储任何数据,因为它使用基本节点类型,而节点是实际节点)。

评论

0赞 Ted Lyngmo 7/8/2023
我会小心像“标准容器使用...”这样的陈述,因为周围有许多标准库实现,它们不需要使用这种技术。尽管如此,这是对 OP 问题的一个非常有用的答案。