【问题标题】:How to implement an STL-style iterator and avoid common pitfalls?如何实现 STL 风格的迭代器并避免常见的陷阱?
【发布时间】:2022-01-06 17:10:12
【问题描述】:

我创建了一个集合,我想为其提供一个 STL 风格的随机访问迭代器。我正在四处寻找迭代器的示例实现,但没有找到。我知道 []* 运算符的 const 重载的需要。迭代器要成为“STL 风格”的要求是什么?还有哪些其他需要避免的陷阱(如果有的话)?

附加上下文:这是一个库,我不想引入任何对它的依赖,除非我真的需要。我编写了自己的集合,以便能够使用相同的编译器提供 C++03 和 C++11 之间的二进制兼容性(因此没有可能会破坏的 STL)。

【问题讨论】:

标签: c++ iterator const-iterator


【解决方案1】:

来自 Boost.Iterator 的 iterator_facade documentation 提供了一个看起来不错的关于为链表实现迭代器的教程。您能否以此为起点在您的容器上构建随机访问迭代器?

如果不出意外,您可以查看iterator_facade 提供的成员函数和 typedef,并将其作为构建自己的起点。

【讨论】:

    【解决方案2】:

    Thomas Becker 就该主题写了一篇有用的文章 here

    之前在 SO 上也出现过这种(可能更简单)的方法:How to correctly implement custom iterators and const_iterators?

    【讨论】:

      【解决方案3】:

      首先,您可以查看 here 以获取各个迭代器类型需要支持的各种操作的列表。

      接下来,当您创建迭代器类时,您需要为它专门化 std::iterator_traits 并提供一些必要的 typedefs(如 iterator_categoryvalue_type),或者从 std::iterator 派生它为您定义了所需的typedefs,因此可以与默认的std::iterator_traits 一起使用。

      免责声明:我知道有些人不太喜欢cplusplus.com,但他们提供了一些非常有用的信息。

      【讨论】:

      • 我真的不明白 cplusplus 与 cppreference 的争论,它们都很好,但缺少很多东西。但是,C++ 是唯一一种实现标准库迭代器的语言是地狱 XD。大多数时候,在 stl 容器上编写包装类比实现迭代器更简单 XD
      • @GameDeveloper 检查我为实现迭代器编写的这个模板库:github.com/VinGarcia/Simple-Iterator-Template。它非常简单,只需要大约 10 行代码即可编写一个迭代器。
      • 不错的类,我很感激,它可能值得移植它来编译非 STL 容器(EA_STL,UE4)。考虑一下! :)
      • 无论如何,如果唯一的原因是cplusplus.com提供了一些真正有用的信息,cppreference.com提供了更多更有用的信息......
      • @L.F.然后随时返回并将该信息添加到该站点的 2011 版本。 ;-)
      【解决方案4】:

      http://www.cplusplus.com/reference/std/iterator/ 有一个方便的图表,详细说明了 C++11 标准第 24.2.2 节的规范。基本上,迭代器具有描述有效操作的标签,并且标签具有层次结构。下面纯粹是象征性的,这些类实际上并不存在。

      iterator {
          iterator(const iterator&);
          ~iterator();
          iterator& operator=(const iterator&);
          iterator& operator++(); //prefix increment
          reference operator*() const;
          friend void swap(iterator& lhs, iterator& rhs); //C++11 I think
      };
      
      input_iterator : public virtual iterator {
          iterator operator++(int); //postfix increment
          value_type operator*() const;
          pointer operator->() const;
          friend bool operator==(const iterator&, const iterator&);
          friend bool operator!=(const iterator&, const iterator&); 
      };
      //once an input iterator has been dereferenced, it is 
      //undefined to dereference one before that.
      
      output_iterator : public virtual iterator {
          reference operator*() const;
          iterator operator++(int); //postfix increment
      };
      //dereferences may only be on the left side of an assignment
      //once an output iterator has been dereferenced, it is 
      //undefined to dereference one before that.
      
      forward_iterator : input_iterator, output_iterator {
          forward_iterator();
      };
      //multiple passes allowed
      
      bidirectional_iterator : forward_iterator {
          iterator& operator--(); //prefix decrement
          iterator operator--(int); //postfix decrement
      };
      
      random_access_iterator : bidirectional_iterator {
          friend bool operator<(const iterator&, const iterator&);
          friend bool operator>(const iterator&, const iterator&);
          friend bool operator<=(const iterator&, const iterator&);
          friend bool operator>=(const iterator&, const iterator&);
      
          iterator& operator+=(size_type);
          friend iterator operator+(const iterator&, size_type);
          friend iterator operator+(size_type, const iterator&);
          iterator& operator-=(size_type);  
          friend iterator operator-(const iterator&, size_type);
          friend difference_type operator-(iterator, iterator);
      
          reference operator[](size_type) const;
      };
      
      contiguous_iterator : random_access_iterator { //C++17
      }; //elements are stored contiguously in memory.
      

      您可以专门化std::iterator_traits&lt;youriterator&gt;,或将相同的typedefs 放入迭代器本身,或从std::iterator(具有这些typedefs)继承。我更喜欢第二种选择,以避免更改 std 命名空间中的内容,并且为了可读性,但大多数人都继承自 std::iterator

      struct std::iterator_traits<youriterator> {        
          typedef ???? difference_type; //almost always ptrdiff_t
          typedef ???? value_type; //almost always T
          typedef ???? reference; //almost always T& or const T&
          typedef ???? pointer; //almost always T* or const T*
          typedef ???? iterator_category;  //usually std::forward_iterator_tag or similar
      };
      

      请注意,iterator_category 应该是 std::input_iterator_tagstd::output_iterator_tagstd::forward_iterator_tagstd::bidirectional_iterator_tagstd::random_access_iterator_tag 之一,具体取决于您的迭代器满足哪些要求。根据您的迭代器,您也可以选择专门化 std::nextstd::prevstd::advancestd::distance,但这很少需要。在极为罕见的情况下,您可能希望专攻std::beginstd::end

      您的容器可能还应该有一个const_iterator,它是一个(可能是可变的)常量数据的迭代器,类似于您的iterator,除了它应该可以从iterator 隐式构造并且用户应该无法修改数据。它的内部指针通常是指向非常量数据的指针,并且有iterator继承自const_iterator,以尽量减少代码重复。

      我在Writing your own STL Container 的帖子有一个更完整的容器/迭代器原型。

      【讨论】:

      • 除了专门化 std::iterator_traits 或自己定义 typedef 之外,您还可以从 std::iterator 派生,它会根据模板参数为您定义这些。
      • @LokiAstari:完整的文档非常广泛(草稿中有 40 页),并且不在 Stack Overflow 的范围内。但是,我添加了详细说明迭代器标签和const_iterator 的更多信息。我的帖子还缺少什么?您似乎暗示要向该类添加更多内容,但问题具体是关于实现迭代器。
      • std::iteratorproposed to be deprecated in C++17;不是,但我不会指望它存在更长时间。
      • @einpoklum 评论的更新:std::iterator 毕竟已被弃用。
      • 请注意,从 C++20 开始,将不再允许对来自 std 命名空间的函数进行特化。见namespace.std
      【解决方案5】:

      出于不同的原因(部分是教育,部分是限制),我曾经/现在和你在同一条船上。我不得不重新编写标准库的所有容器,并且容器必须符合标准。这意味着,如果我将我的容器换成 stl 版本,那么代码的工作方式是一样的。这也意味着我必须重新编写迭代器。

      不管怎样,我看了EASTL。除了学习大量关于容器的知识外,我一直使用 stl 容器或通过我的本科课程来学习这些容器。主要原因是 EASTL 比对应的 stl 更具可读性(我发现这仅仅是因为缺少所有的宏和直接的编码风格)。里面有一些令人讨厌的东西(比如#ifdefs 表示例外),但没有什么能让你不知所措。

      正如其他人提到的,请查看 cplusplus.com 关于迭代器和容器的参考。

      【讨论】:

        【解决方案6】:

        这里是原始指针迭代器的示例。

        您不应该使用迭代器类来处理原始指针!

        #include <iostream>
        #include <vector>
        #include <list>
        #include <iterator>
        #include <assert.h>
        
        template<typename T>
        class ptr_iterator
            : public std::iterator<std::forward_iterator_tag, T>
        {
            typedef ptr_iterator<T>  iterator;
            pointer pos_;
        public:
            ptr_iterator() : pos_(nullptr) {}
            ptr_iterator(T* v) : pos_(v) {}
            ~ptr_iterator() {}
        
            iterator  operator++(int) /* postfix */         { return pos_++; }
            iterator& operator++()    /* prefix */          { ++pos_; return *this; }
            reference operator* () const                    { return *pos_; }
            pointer   operator->() const                    { return pos_; }
            iterator  operator+ (difference_type v)   const { return pos_ + v; }
            bool      operator==(const iterator& rhs) const { return pos_ == rhs.pos_; }
            bool      operator!=(const iterator& rhs) const { return pos_ != rhs.pos_; }
        };
        
        template<typename T>
        ptr_iterator<T> begin(T *val) { return ptr_iterator<T>(val); }
        
        
        template<typename T, typename Tsize>
        ptr_iterator<T> end(T *val, Tsize size) { return ptr_iterator<T>(val) + size; }
        

        基于原始指针范围的循环解决方法。请纠正我,如果有更好的方法可以从原始指针创建基于范围的循环。

        template<typename T>
        class ptr_range
        {
            T* begin_;
            T* end_;
        public:
            ptr_range(T* ptr, size_t length) : begin_(ptr), end_(ptr + length) { assert(begin_ <= end_); }
            T* begin() const { return begin_; }
            T* end() const { return end_; }
        };
        
        template<typename T>
        ptr_range<T> range(T* ptr, size_t length) { return ptr_range<T>(ptr, length); }
        

        简单测试

        void DoIteratorTest()
        {
            const static size_t size = 10;
            uint8_t *data = new uint8_t[size];
            {
                // Only for iterator test
                uint8_t n = '0';
                auto first = begin(data);
                auto last = end(data, size);
                for (auto it = first; it != last; ++it)
                {
                    *it = n++;
                }
        
                // It's prefer to use the following way:
                for (const auto& n : range(data, size))
                {
                    std::cout << " char: " << static_cast<char>(n) << std::endl;
                }
            }
            {
                // Only for iterator test
                ptr_iterator<uint8_t> first(data);
                ptr_iterator<uint8_t> last(first + size);
                std::vector<uint8_t> v1(first, last);
        
                // It's prefer to use the following way:
                std::vector<uint8_t> v2(data, data + size);
            }
            {
                std::list<std::vector<uint8_t>> queue_;
                queue_.emplace_back(begin(data), end(data, size));
                queue_.emplace_back(data, data + size);
            }
        }
        

        【讨论】:

          【解决方案7】:

          我试图解决能够迭代几个不同的文本数组的问题,所有这些文本数组都存储在一个很大的内存驻留数据库中,struct

          以下是在 MFC 测试应用程序上使用 Visual Studio 2017 社区版制定的。我将其作为示例包括在内,因为此帖子是我遇到的几个提供了一些帮助但仍不足以满足我的需求的帖子之一。

          包含内存驻留数据的struct 如下所示。为简洁起见,我删除了大部分元素,也没有包括使用的预处理器定义(使用的 SDK 用于 C 和 C++,并且是旧的)。

          我感兴趣的是为各种 WCHAR 二维数组创建迭代器,其中包含用于助记符的文本字符串。

          typedef struct  tagUNINTRAM {
              // stuff deleted ...
              WCHAR   ParaTransMnemo[MAX_TRANSM_NO][PARA_TRANSMNEMO_LEN]; /* prog #20 */
              WCHAR   ParaLeadThru[MAX_LEAD_NO][PARA_LEADTHRU_LEN];   /* prog #21 */
              WCHAR   ParaReportName[MAX_REPO_NO][PARA_REPORTNAME_LEN];   /* prog #22 */
              WCHAR   ParaSpeMnemo[MAX_SPEM_NO][PARA_SPEMNEMO_LEN];   /* prog #23 */
              WCHAR   ParaPCIF[MAX_PCIF_SIZE];            /* prog #39 */
              WCHAR   ParaAdjMnemo[MAX_ADJM_NO][PARA_ADJMNEMO_LEN];   /* prog #46 */
              WCHAR   ParaPrtModi[MAX_PRTMODI_NO][PARA_PRTMODI_LEN];  /* prog #47 */
              WCHAR   ParaMajorDEPT[MAX_MDEPT_NO][PARA_MAJORDEPT_LEN];    /* prog #48 */
              //  ... stuff deleted
          } UNINIRAM;
          

          当前的方法是使用模板为每个数组定义一个代理类,然后拥有一个迭代器类,该类可用于通过使用代表该数组的代理对象来迭代特定数组。

          内存常驻数据的副本存储在一个对象中,该对象处理从/向磁盘读取和写入内存常驻数据。此类CFilePara 包含模板化代理类(MnemonicIteratorDimSize 及其派生的子类MnemonicIteratorDimSizeBase)和迭代器类MnemonicIterator

          创建的代理对象附加到一个迭代器对象,该迭代器对象通过一个基类描述的接口访问必要的信息,所有代理类都从该基类派生。结果是有一个单一类型的迭代器类,它可以与多个不同的代理类一起使用,因为不同的代理类都公开了相同的接口,即代理基类的接口。

          第一件事是创建一组标识符,这些标识符将提供给类工厂以生成该类型助记符的特定代理对象。这些标识符用作用户界面的一部分,以识别用户有兴趣查看和可能修改的特定供应数据。

          const static DWORD_PTR dwId_TransactionMnemonic = 1;
          const static DWORD_PTR dwId_ReportMnemonic = 2;
          const static DWORD_PTR dwId_SpecialMnemonic = 3;
          const static DWORD_PTR dwId_LeadThroughMnemonic = 4;
          

          代理类

          模板化代理类及其基类如下。我需要容纳几种不同类型的wchar_t 文本字符串数组。二维数组具有不同数量的助记符,具体取决于助记符的类型(目的),并且不同类型的助记符具有不同的最大长度,在 5 个文本字符和 20 个文本字符之间变化。派生代理类的模板自然适合需要每个助记符中最大字符数的模板。代理对象创建完成后,我们再使用SetRange()方法来指定实际的助记符数组及其范围。

          // proxy object which represents a particular subsection of the
          // memory resident database each of which is an array of wchar_t
          // text arrays though the number of array elements may vary.
          class MnemonicIteratorDimSizeBase
          {
              DWORD_PTR  m_Type;
          
          public:
              MnemonicIteratorDimSizeBase(DWORD_PTR x) { }
              virtual ~MnemonicIteratorDimSizeBase() { }
          
              virtual wchar_t *begin() = 0;
              virtual wchar_t *end() = 0;
              virtual wchar_t *get(int i) = 0;
              virtual int ItemSize() = 0;
              virtual int ItemCount() = 0;
          
              virtual DWORD_PTR ItemType() { return m_Type; }
          };
          
          template <size_t sDimSize>
          class MnemonicIteratorDimSize : public MnemonicIteratorDimSizeBase
          {
              wchar_t    (*m_begin)[sDimSize];
              wchar_t    (*m_end)[sDimSize];
          
          public:
              MnemonicIteratorDimSize(DWORD_PTR x) : MnemonicIteratorDimSizeBase(x), m_begin(0), m_end(0) { }
              virtual ~MnemonicIteratorDimSize() { }
          
              virtual wchar_t *begin() { return m_begin[0]; }
              virtual wchar_t *end() { return m_end[0]; }
              virtual wchar_t *get(int i) { return m_begin[i]; }
          
              virtual int ItemSize() { return sDimSize; }
              virtual int ItemCount() { return m_end - m_begin; }
          
              void SetRange(wchar_t (*begin)[sDimSize], wchar_t (*end)[sDimSize]) {
                  m_begin = begin; m_end = end;
              }
          
          };
          

          迭代器类

          迭代器类本身如下。这个类只提供了基本的前向迭代器功能,这是目前所需要的。但是,我希望当我需要额外的东西时,这会改变或扩展。

          class MnemonicIterator
          {
          private:
              MnemonicIteratorDimSizeBase   *m_p;  // we do not own this pointer. we just use it to access current item.
              int      m_index;                    // zero based index of item.
              wchar_t  *m_item;                    // value to be returned.
          
          public:
              MnemonicIterator(MnemonicIteratorDimSizeBase *p) : m_p(p) { }
              ~MnemonicIterator() { }
          
              // a ranged for needs begin() and end() to determine the range.
              // the range is up to but not including what end() returns.
              MnemonicIterator & begin() { m_item = m_p->get(m_index = 0); return *this; }                 // begining of range of values for ranged for. first item
              MnemonicIterator & end() { m_item = m_p->get(m_index = m_p->ItemCount()); return *this; }    // end of range of values for ranged for. item after last item.
              MnemonicIterator & operator ++ () { m_item = m_p->get(++m_index); return *this; }            // prefix increment, ++p
              MnemonicIterator & operator ++ (int i) { m_item = m_p->get(m_index++); return *this; }       // postfix increment, p++
              bool operator != (MnemonicIterator &p) { return **this != *p; }                              // minimum logical operator is not equal to
              wchar_t * operator *() const { return m_item; }                                              // dereference iterator to get what is pointed to
          };
          

          代理对象工厂根据助记符确定创建哪个对象。创建代理对象,返回的指针是标准的基类类型,以便无论访问哪个不同的助记符部分都具有统一的接口。 SetRange()方法用于向代理对象指定代理所代表的具体数组元素以及数组元素的范围。

          CFilePara::MnemonicIteratorDimSizeBase * CFilePara::MakeIterator(DWORD_PTR x)
          {
              CFilePara::MnemonicIteratorDimSizeBase  *mi = nullptr;
          
              switch (x) {
              case dwId_TransactionMnemonic:
                  {
                      CFilePara::MnemonicIteratorDimSize<PARA_TRANSMNEMO_LEN> *mk = new CFilePara::MnemonicIteratorDimSize<PARA_TRANSMNEMO_LEN>(x);
                      mk->SetRange(&m_Para.ParaTransMnemo[0], &m_Para.ParaTransMnemo[MAX_TRANSM_NO]);
                      mi = mk;
                  }
                  break;
              case dwId_ReportMnemonic:
                  {
                      CFilePara::MnemonicIteratorDimSize<PARA_REPORTNAME_LEN> *mk = new CFilePara::MnemonicIteratorDimSize<PARA_REPORTNAME_LEN>(x);
                      mk->SetRange(&m_Para.ParaReportName[0], &m_Para.ParaReportName[MAX_REPO_NO]);
                      mi = mk;
                  }
                  break;
              case dwId_SpecialMnemonic:
                  {
                      CFilePara::MnemonicIteratorDimSize<PARA_SPEMNEMO_LEN> *mk = new CFilePara::MnemonicIteratorDimSize<PARA_SPEMNEMO_LEN>(x);
                      mk->SetRange(&m_Para.ParaSpeMnemo[0], &m_Para.ParaSpeMnemo[MAX_SPEM_NO]);
                      mi = mk;
                  }
                  break;
              case dwId_LeadThroughMnemonic:
                  {
                      CFilePara::MnemonicIteratorDimSize<PARA_LEADTHRU_LEN> *mk = new CFilePara::MnemonicIteratorDimSize<PARA_LEADTHRU_LEN>(x);
                      mk->SetRange(&m_Para.ParaLeadThru[0], &m_Para.ParaLeadThru[MAX_LEAD_NO]);
                      mi = mk;
                  }
                  break;
              }
          
              return mi;
          }
          

          使用代理类和迭代器

          如下循环所示,使用代理类及其迭代器,用助记符列表填充CListCtrl 对象。我正在使用std::unique_ptr,这样当我不再需要代理类并且std::unique_ptr 超出范围时,内存将被清理。

          此源代码的作用是为struct 内的数组创建一个代理对象,该对象对应于指定的助记符标识符。然后,它为该对象创建一个迭代器,使用范围 for 填充 CListCtrl 控件,然后进行清理。这些都是原始的wchar_t 文本字符串,可能正是数组元素的数量,因此我们将字符串复制到临时缓冲区中以确保文本以零结尾。

              std::unique_ptr<CFilePara::MnemonicIteratorDimSizeBase> pObj(pFile->MakeIterator(m_IteratorType));
              CFilePara::MnemonicIterator pIter(pObj.get());  // provide the raw pointer to the iterator who doesn't own it.
          
              int i = 0;    // CListCtrl index for zero based position to insert mnemonic.
              for (auto x : pIter)
              {
                  WCHAR szText[32] = { 0 };     // Temporary buffer.
          
                  wcsncpy_s(szText, 32, x, pObj->ItemSize());
                  m_mnemonicList.InsertItem(i, szText);  i++;
              }
          

          【讨论】:

            【解决方案8】:

            现在是基于范围的 for 循环的键迭代器。

            template<typename C>
            class keys_it
            {
                typename C::const_iterator it_;
            public:
                using key_type        = typename C::key_type;
                using pointer         = typename C::key_type*;
                using difference_type = std::ptrdiff_t;
            
                keys_it(const typename C::const_iterator & it) : it_(it) {}
            
                keys_it         operator++(int               ) /* postfix */ { return it_++         ; }
                keys_it&        operator++(                  ) /*  prefix */ { ++it_; return *this  ; }
                const key_type& operator* (                  ) const         { return it_->first    ; }
                const key_type& operator->(                  ) const         { return it_->first    ; }
                keys_it         operator+ (difference_type v ) const         { return it_ + v       ; }
                bool            operator==(const keys_it& rhs) const         { return it_ == rhs.it_; }
                bool            operator!=(const keys_it& rhs) const         { return it_ != rhs.it_; }
            };
            
            template<typename C>
            class keys_impl
            {
                const C & c;
            public:
                keys_impl(const C & container) : c(container) {}
                const keys_it<C> begin() const { return keys_it<C>(std::begin(c)); }
                const keys_it<C> end  () const { return keys_it<C>(std::end  (c)); }
            };
            
            template<typename C>
            keys_impl<C> keys(const C & container) { return keys_impl<C>(container); }
            

            用法:

            std::map<std::string,int> my_map;
            // fill my_map
            for (const std::string & k : keys(my_map))
            {
                // do things
            }
            

            这就是我想要的。但似乎没有人拥有它。

            你得到我的强迫症代码对齐作为奖励。

            作为练习,为values(my_map)写你自己的

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 1970-01-01
              • 2010-12-26
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2010-11-16
              • 1970-01-01
              相关资源
              最近更新 更多