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

306

Hice una colección para la que quiero proporcionar un iterador de acceso aleatorio estilo STL. Estaba buscando un ejemplo de implementación de un iterador pero no encontré ninguno. Sé sobre la necesidad de sobrecargas []y *operadores constantes . ¿Cuáles son los requisitos para que un iterador sea "STL-style" y cuáles son otras dificultades para 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 rompa).

Tamás Szelei
fuente
13
+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.
jalf
2
Recuerda también que tus iteradores tienen que ser ASUSTADOS. boost.org/doc/libs/1_55_0/doc/html/intrusive/…
alfC

Respuestas:

232

http://www.cplusplus.com/reference/std/iterator/ tiene una útil tabla 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.

Puede especializarse std::iterator_traits<youriterator>o poner los mismos typedefs en el iterador, o heredar de std::iterator(que tiene estos typedefs). Prefiero la segunda opción, para evitar cambiar las cosas en el stdespacio de nombres, y para facilitar la lectura, pero la mayoría de las personas heredan 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 el iterator_category debería ser uno de std::input_iterator_tag, std::output_iterator_tag, std::forward_iterator_tag, std::bidirectional_iterator_tag, o std::random_access_iterator_tag, dependiendo de qué requisitos de sus iterador satisface. Dependiendo de su iterador, puede optar por especializarse std::next, std::prev, std::advance, y std::distance, así, pero esto rara vez es necesaria. En casos extremadamente raros , es posible que desee especializarse std::beginy std::end.

Probablemente, su contenedor también debería tener un const_iteratoriterador (posiblemente mutable) de datos constantes que sea similar al suyo, iteratorexcepto que debería ser implícitamente construible desde ay los iteratorusuarios deberían ser incapaces de modificar los datos. Es común que su puntero interno sea un puntero a datos no constantes y que haya iteratorheredado const_iteratorpara minimizar la duplicación de código.

Mi publicación en Writing your own STL Container tiene un prototipo más completo de contenedor / iterador.

Pato mugido
fuente
2
Además de especializarse std::iterator_traitso definir los typedefs usted mismo, también puede derivar de std::iterator, que los define por usted, dependiendo de los parámetros de su plantilla.
Christian Rau
3
@LokiAstari: la documentación completa es bastante extensa (páginas 40 en el borrador), y no está dentro del alcance de Stack Overflow. Sin embargo, agregué más información que detalla las etiquetas de iterador y const_iterator. ¿Qué más faltaba en 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.
Mooing Duck
55
std::iteratorse propuso ser obsoleto en C ++ 17 ; no lo era, pero no confiaría en que estuviera por mucho más tiempo.
einpoklum
2
Una actualización del comentario de @einpoklum: std::iteratorfue desaprobado después de todo.
scry
1
@ JonathanLee: Wow, eso operator booles increíblemente peligroso. Alguien intentará usar eso para detectar el final de un rango while(it++), pero todo lo que realmente verifica es si el iterador se construyó con un parámetro.
Mooing Duck
16

La 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?

Por lo menos, puede echar un vistazo a las funciones de miembro y las definiciones de tipo proporcionadas por iterator_facadey usarlo como punto de partida para construir el suyo.

Michael Kristofik
fuente
10

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

¡No deberías usar la clase iteradora para trabajar con punteros sin formato!

#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 de bucle basado en rango de puntero sin formato. Por favor, corríjame, 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);
    }
}
Valdemar_Rudolfovich
fuente
5

En primer lugar, puede buscar aquí 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_traitsy proporcionar algunos typedefs necesarios (like iterator_categoryo value_type) o, alternativamente, derivarlos std::iterator, lo que define los typedefs necesarios para usted y, por lo tanto, puede usarse con el valor predeterminado std::iterator_traits.

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

Christian Rau
fuente
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 de contenedor sobre un contenedor stl que implementar un iterador XD
CoffeDeveloper
@GameDeveloper revise esta biblioteca de plantillas que escribí para implementar iteradores: github.com/VinGarcia/Simple-Iterator-Template . Es muy simple y requiere solo unas 10 líneas de código para escribir un iterador.
VinGarcia
Buena clase, lo aprecio, probablemente valga la pena portarlo para compilarlo también con contenedores que no sean STL (EA_STL, UE4). ¡Considérelo! :)
CoffeDeveloper
De todos modos, si la única razón es que cplusplus.com proporciona información realmente útil, cppreference.com proporciona 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
3

Estuve / estoy en el mismo barco que usted por diferentes razones (en parte educativas, en parte limitantes). Tuve que volver a escribir todos los contenedores de la biblioteca estándar y los contenedores tenían que cumplir con el estándar. Eso significa que, si cambio mi contenedor con la versión stl , el código funcionaría igual. Lo que también significaba que tenía que volver a escribir los iteradores.

De todos modos, miré a EASTL . Además de aprender mucho sobre contenedores que nunca aprendí todo este tiempo usando los contenedores stl o en mis cursos de pregrado. La razón principal es que EASTL es más legible que la contraparte stl (descubrí que esto se debe simplemente a la falta de todas las macros y al estilo de codificación directo). Hay algunas cosas repugnantes (como #ifdefs para excepciones) pero nada que lo abrume.

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

Samaursa
fuente
3

Estaba tratando de resolver el problema de poder iterar sobre varios arreglos de texto diferentes, todos los cuales están almacenados en una base de datos residente de memoria que es grande struct.

Lo siguiente se resolvió utilizando 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 varias que encontré y que me brindó ayuda, pero aún no era suficiente para mis necesidades.

El structcontenido de los datos residentes de 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 el preprocesador que se utiliza (el SDK en uso es para C y C ++ y es antiguo).

Lo que me interesaba era tener iteradores para las diversas WCHARmatrices bidimensionales que contenían cadenas de texto para mnemotécnicos.

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 pueda usar para iterar sobre una matriz particular mediante el uso de un objeto proxy que represente 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, CFileParacontiene la clase de proxy con plantilla ( MnemonicIteratorDimSizey la clase sub partir de la cual es 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 las diferentes clases de proxy exponen la misma interfaz, la interfaz de la clase base de proxy.

Lo primero era 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 mnemotécnico. 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 de proxy

La clase proxy con plantilla y su clase base son las siguientes. Necesitaba acomodar varios tipos diferentes de wchar_tmatrices 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 proxy derivada encajaban naturalmente con la plantilla que requería la cantidad máxima de caracteres en cada mnemónica. Después de crear el objeto proxy, usamos el SetRange()método para especificar la matriz mnemónica 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 iteradora en sí es la siguiente. Esta clase proporciona la funcionalidad básica de iterador directo, 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 proxy determina qué objeto crear en función del identificador mnemónico. El objeto proxy se crea y el puntero devuelto es el tipo de clase base estándar para tener una interfaz uniforme independientemente de a cuál de las diferentes secciones mnemotécnicas se accede. El SetRange()método se utiliza para especificar al objeto proxy los elementos de 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 proxy y su iterador se usan como se muestra en el siguiente ciclo para completar un CListCtrlobjeto con una lista de mnemotécnicos. Estoy usando std::unique_ptrpara que cuando la clase proxy ya no la necesite y std::unique_ptrquede fuera de alcance, la memoria se limpie.

Lo que hace este código fuente es crear un objeto proxy para la matriz dentro del structcual corresponde al identificador mnemotécnico especificado. Luego crea un iterador para ese objeto, usa un rango forpara completar el CListCtrlcontrol y luego limpia. Estas son todas wchar_tcadenas de texto sin procesar 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 esté terminado 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++;
    }
Richard Chambers
fuente
1

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

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 parece que nadie lo tenía.

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

Como ejercicio, escribe el tuyo para values(my_map)

Gabriel
fuente