¿Cómo implementar un iterador de estilo STL y evitar errores comunes?

Hice una colección para la que quiero proporcionar un iterador de acceso aleatorio de estilo STL. Estaba buscando una implementación de ejemplo de un iterador pero no encontré ninguna. Sé sobre la necesidad de sobrecargas constantes de [] y * operadores. ¿Cuáles son los requisitos para que un iterador sea "estilo STL" y cuáles son algunas otras trampas que se deben evitar (si las hay)?

Contexto adicional: esto es para una biblioteca y no quiero introducir ninguna dependencia a menos que realmente lo necesite. Escribo mi propia colección para poder proporcionar compatibilidad binaria entre C ++ 03 y C ++ 11 con el mismo compilador (por lo que no hay STL que probablemente se rompería).

preguntado el 08 de noviembre de 11 a las 17:11

+1! Buena pregunta. Me he preguntado lo mismo. Es bastante fácil juntar algo basado en Boost.Iterator, pero es sorprendentemente difícil encontrar una lista de los requisitos si lo implementa desde cero. -

Recuerde también que sus iteradores deben ser ASUSTOSOS. boost.org/doc/libs/1_55_0/doc/html/intrusive/… -

8 Respuestas

http://www.cplusplus.com/reference/std/iterator/ tiene un gráfico útil que detalla las especificaciones del § 24.2.2 del estándar C ++ 11. Básicamente, los iteradores tienen etiquetas que describen las operaciones válidas y las etiquetas tienen una jerarquía. A continuación es puramente simbólico, estas clases en realidad no existen como tales.

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.

Puedes especializarte std::iterator_traits<youriterator>, o poner las mismas definiciones de tipo en el propio iterador, o heredar de std::iterator (que tiene estos typedefs). Prefiero la segunda opción, para evitar cambiar las cosas en el std espacio de nombres y para facilitar la lectura, pero la mayoría de las personas heredan de 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
};

Tenga en cuenta que iterator_category debe ser uno de std::input_iterator_tag, std::output_iterator_tag, std::forward_iterator_tag, std::bidirectional_iterator_tagy std::random_access_iterator_tag, dependiendo de los requisitos que satisfaga su iterador. Dependiendo de su iterador, puede optar por especializarse std::next, std::prev, std::advancey std::distance también, pero esto rara vez es necesario. En extremadamente raro casos en los que tal vez desee especializarse std::begin y std::end.

Su contenedor probablemente también debería tener un const_iterator, que es un iterador (posiblemente mutable) de datos constantes que es similar a su iterator excepto que debe ser implícitamente construible a partir de un iterator y los usuarios no deberían poder modificar los datos. Es común que su puntero interno sea un puntero a datos no constantes y tenga iterator heredar de const_iterator para minimizar la duplicación de código.

Mi publicación en Escribir su propio contenedor STL tiene un prototipo de contenedor / iterador más completo.

Respondido el 02 de diciembre de 18 a las 23:12

Además de especializarse std::iterator_traits o definir el typedefs usted mismo, también puede derivar de std::iterator, que los define para usted, en función de los parámetros de su plantilla. - Christian Rau

@LokiAstari: La documentación completa es bastante extensa (40 páginas en el borrador), y no está en el alcance de Stack Overflow. Sin embargo, agregué más información que detalla las etiquetas del iterador y const_iterator. ¿Qué más le faltaba a mi publicación? Parece implicar que hay más para agregar a la clase, pero la pregunta es específicamente sobre la implementación de iteradores. - Pato morando

std::iterator fue propuesto para ser obsoleto en C ++ 17; no lo fue, pero no me atrevería a confiar en que estaría presente por mucho más tiempo. - einpoklum

Una actualización del comentario de @ einpoklum: std::iterator después de todo estaba en desuso. - adivinar

@JonathanLee: Vaya, eso operator bool es increíblemente peligroso. Alguien intentará usar eso para detectar el final de un rango while(it++), pero todo lo que realmente comprueba es si el iterador se construyó con un parámetro. - Pato morando

El documentación de iterator_facade de Boost.Iterator proporciona lo que parece un buen tutorial sobre la implementación de iteradores para una lista vinculada. ¿Podría usar eso como punto de partida para construir un iterador de acceso aleatorio sobre su contenedor?

Si nada más, puede echar un vistazo a las funciones miembro y typedefs proporcionadas por iterator_facade y utilícelo como punto de partida para crear el suyo propio.

respondido 08 nov., 11:21

Aquí hay una muestra de iterador de puntero sin formato.

¡No debería usar la clase de iterador para trabajar con punteros sin procesar!

#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; }

Solución alternativa de bucle basado en rango de puntero sin procesar. Por favor, corrígeme, si hay una mejor manera de hacer un bucle basado en rango desde un puntero sin formato.

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); }

Y prueba simple

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);
    }
}

Respondido el 29 de Septiembre de 16 a las 12:09

Thomas Becker escribió un artículo útil sobre el tema. aquí.

También hubo este enfoque (quizás más simple) que apareció anteriormente en SO: ¿Cómo implementar correctamente iteradores personalizados y const_iterators?

contestado el 23 de mayo de 17 a las 15:05

Primero que nada puedes mirar aquí para obtener una lista de las diversas operaciones que los tipos de iteradores individuales deben admitir.

A continuación, cuando haya creado su clase de iterador, debe especializarse std::iterator_traits para ello y proporcionar algunos necesarios typedefs (como iterator_category or value_type) o, alternativamente, derivarlo de std::iterator, que define la necesidad typedefs para usted y, por lo tanto, se puede utilizar con el std::iterator_traits.

Descargo de responsabilidad: Sé que a algunas personas no les gusta cplusplus.com tanto, pero brindan información realmente útil al respecto.

contestado el 23 de mayo de 19 a las 13:05

Realmente no entiendo la disputa cplusplus vs cppreference, ambos son buenos y faltan muchas cosas. Sin embargo, C ++ es el único lenguaje donde implementar iteradores de biblioteca estándar es un infierno XD. La mayoría de las veces es más simple escribir una clase contenedora sobre un contenedor stl que implementar un iterador XD - CaféDesarrollador

@GameDeveloper verifica esta biblioteca de plantillas que escribí para implementar iteradores: github.com/VinGarcia/Simple-Iterator-Template. Es muy simple y solo requiere alrededor de 10 líneas de código para escribir un iterador. - VinGarcia

Buena clase, lo agradezco, probablemente valga la pena portarlo para compilarlo también con contenedores que no sean STL (EA_STL, UE4). ¡Considérelo! :) - CaféDesarrollador

De todos modos, si la única razón es que cplusplus.com proporciona información realmente útil, cppreference.com proporciona más información más útil ... - LF

@LF Entonces, siéntase libre de retroceder en el tiempo y agregar esa información a la versión 2011 del sitio. ;-) - Christian Rau

Estuve / estoy en el mismo barco que tú por diferentes razones (en parte educativas, en parte por limitaciones). Tuve que volver a escribir todos los contenedores de la biblioteca estándar y los contenedores debían ajustarse al estándar. Eso significa que si cambio mi contenedor con el Stl versión, el código funcionaría igual. Lo que también significó que tuve que volver a escribir los iteradores.

De todos modos, miré EASTL. Aparte de aprender un montón sobre contenedores que nunca aprendí en todo este tiempo usando el Stl contenedores oa través de mis cursos de pregrado. La principal razón es que EASTL es más legible que el Stl contraparte (descubrí que esto se debe simplemente a la falta de todas las macros y al estilo de codificación sencillo). Hay algunas cosas repugnantes allí (como #ifdefs para las excepciones) pero nada que te abrume.

Como otros mencionaron, mire la referencia de cplusplus.com sobre iteradores y contenedores.

respondido 30 nov., 16:12

Estaba tratando de resolver el problema de poder iterar sobre varias matrices de texto diferentes, todas las cuales están almacenadas dentro de una base de datos residente en memoria que es una gran struct.

Lo siguiente se resolvió con Visual Studio 2017 Community Edition en una aplicación de prueba MFC. Incluyo esto como un ejemplo, ya que esta publicación fue una de las varias que encontré que me brindaron algo de ayuda pero que aún eran insuficientes para mis necesidades.

El struct que contenía los datos residentes en la memoria se parecía a lo siguiente. He eliminado la mayoría de los elementos en aras de la brevedad y tampoco he incluido las definiciones del preprocesador utilizadas (el SDK en uso es para C y C ++ y es antiguo).

Lo que estaba interesado en hacer es tener iteradores para los distintos WCHAR matrices bidimensionales que contenían cadenas de texto para mnemónicos.

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;

El enfoque actual es usar una plantilla para definir una clase de proxy para cada una de las matrices y luego tener una sola clase de iterador que se puede usar para iterar sobre una matriz en particular mediante el uso de un objeto proxy que representa la matriz.

Una copia de los datos residentes en la memoria se almacena en un objeto que maneja la lectura y escritura de los datos residentes en la memoria desde / hacia el disco. Esta clase, CFilePara contiene la clase de proxy con plantilla (MnemonicIteratorDimSize y la subclase de la que se deriva, MnemonicIteratorDimSizeBase) y la clase de iterador, MnemonicIterator.

El objeto proxy creado se adjunta a un objeto iterador que accede a la información necesaria a través de una interfaz descrita por una clase base de la que se derivan todas las clases proxy. El resultado es tener un solo tipo de clase de iterador que se puede usar con varias clases de proxy diferentes porque todas las clases de proxy diferentes exponen la misma interfaz, la interfaz de la clase base de proxy.

Lo primero fue crear un conjunto de identificadores que se proporcionarían a una fábrica de clases para generar el objeto proxy específico para ese tipo de mnemónico. Estos identificadores se utilizan como parte de la interfaz de usuario para identificar los datos de aprovisionamiento particulares que el usuario está interesado en ver y posiblemente modificar.

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;

La clase Proxy

La clase de proxy con plantilla y su clase base son las siguientes. Necesitaba acomodar varios tipos diferentes de wchar_t matrices de cadenas de texto. Las matrices bidimensionales tenían diferentes números de mnemónicos, dependiendo del tipo (propósito) del mnemónico y los diferentes tipos de mnemónicos tenían diferentes longitudes máximas, variando entre cinco caracteres de texto y veinte caracteres de texto. Las plantillas para la clase de proxy derivada encajaban naturalmente con la plantilla que requería el número máximo de caracteres en cada mnemónico. Una vez creado el objeto proxy, usamos el SetRange() método para especificar la matriz mnemotécnica real y su rango.

// 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;
    }

};

La clase de iterador

La clase de iterador en sí es la siguiente. Esta clase proporciona solo la funcionalidad básica de iterador hacia adelante, que es todo lo que se necesita en este momento. Sin embargo, espero que esto cambie o se extienda cuando necesite algo adicional.

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
};

La fábrica de objetos de proxy determina qué objeto crear en función del identificador mnemónico. Se crea el objeto proxy y el puntero devuelto es el tipo de clase base estándar para tener una interfaz uniforme independientemente de cuál de las diferentes secciones mnemotécnicas se esté accediendo. los SetRange() El método se utiliza para especificar al objeto proxy los elementos de la matriz específicos que representa el proxy y el rango de los elementos de la matriz.

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;
}

Uso de la clase de proxy y el iterador

La clase de proxy y su iterador se utilizan como se muestra en el siguiente ciclo para completar un CListCtrl objeto con una lista de mnemónicos. estoy usando std::unique_ptr para que cuando la clase de proxy ya no la necesite y el std::unique_ptr sale del alcance, la memoria se limpiará.

Lo que hace este código fuente es crear un objeto proxy para la matriz dentro del struct que corresponde al identificador mnemónico especificado. Luego crea un iterador para ese objeto, usa un rango for para completar el CListCtrl control y luego limpia. Estos son todos crudos wchar_t cadenas de texto que pueden ser exactamente el número de elementos de la matriz, por lo que copiamos la cadena en un búfer temporal para asegurarnos de que el texto termine en cero.

    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++;
    }

respondido 12 nov., 18:01

Y ahora un iterador de claves para el bucle for basado en rangos.

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); }

Uso:

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

Eso es lo que estaba buscando. Pero nadie lo tenía, al parecer.

Obtienes mi alineación de código OCD como un bono.

Como ejercicio, escribe el tuyo propio para values(my_map)

Respondido el 30 de diciembre de 19 a las 21:12

No es la respuesta que estás buscando? Examinar otras preguntas etiquetadas or haz tu propia pregunta.