Come implementare un iteratore in stile STL ed evitare insidie ​​comuni?


306

Ho realizzato una raccolta per la quale desidero fornire un iteratore ad accesso casuale in stile STL. Stavo cercando un esempio di implementazione di un iteratore ma non ne ho trovato. Conosco la necessità di sovraccarichi costanti []e di *operatori. Quali sono i requisiti affinché un iteratore sia "stile STL" e quali altre insidie ​​evitare (se presenti)?

Contesto aggiuntivo: questo è per una biblioteca e non voglio introdurre alcuna dipendenza da essa se non ne ho davvero bisogno. Scrivo la mia raccolta per essere in grado di fornire la compatibilità binaria tra C ++ 03 e C ++ 11 con lo stesso compilatore (quindi nessun STL che probabilmente si spezzerebbe).


13
+1! Bella domanda Mi sono chiesto la stessa cosa. È abbastanza facile sfogliare qualcosa insieme basato su Boost.Iterator, ma è sorprendentemente difficile trovare un elenco dei requisiti se lo si implementa da zero.
jalf

2
Ricorda anche che i tuoi iteratori devono essere spaventosi. boost.org/doc/libs/1_55_0/doc/html/intrusive/…
alfC

Risposte:


232

http://www.cplusplus.com/reference/std/iterator/ ha una pratica tabella che dettaglia le specifiche del § 24.2.2 della norma C ++ 11. Fondamentalmente, gli iteratori hanno tag che descrivono le operazioni valide e i tag hanno una gerarchia. Di seguito è puramente simbolico, queste classi in realtà non esistono in quanto tali.

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.

Puoi specializzarti std::iterator_traits<youriterator>o inserire gli stessi typedef nello stesso iteratore o ereditare da std::iterator(che ha questi typedef). Preferisco la seconda opzione, per evitare di cambiare le cose nello stdspazio dei nomi e per leggibilità, ma la maggior parte delle persone eredita 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
};

Si noti l'iterator_category dovrebbe essere uno dei std::input_iterator_tag, std::output_iterator_tag, std::forward_iterator_tag, std::bidirectional_iterator_tag, o std::random_access_iterator_tag, a seconda di quali requisiti tuoi iteratore soddisfa. A seconda del vostro iteratore, si può scegliere di specializzarsi std::next, std::prev, std::advance, e std::distancecosì, ma questo è raramente necessaria. In casi estremamente rari potresti voler specializzarti std::begine std::end.

Il tuo contenitore dovrebbe probabilmente avere anche un const_iterator, che è un (eventualmente mutabile) iteratore di dati costanti simili al tuo iteratortranne che dovrebbe essere implicitamente costruibile da un iteratore gli utenti non dovrebbero essere in grado di modificare i dati. È comune che il suo puntatore interno sia un puntatore a dati non costanti e ne abbia iteratorereditato in const_iteratormodo da minimizzare la duplicazione del codice.

Il mio post su Writing your STL Container ha un prototipo di container / iteratore più completo.


2
Oltre a specializzarti std::iterator_traitso definire tu stesso i typedef, puoi anche derivarne std::iterator, che li definisce, a seconda dei parametri del suo modello.
Christian Rau,

3
@LokiAstari: la documentazione completa è piuttosto estesa (40 pagine nella bozza) e non nell'ambito di Stack Overflow. Tuttavia, ho aggiunto ulteriori informazioni in dettaglio i tag iteratore e const_iterator. Cos'altro mancava al mio post? Sembri implicare che c'è altro da aggiungere alla classe, ma la domanda riguarda specificamente l'implementazione di iteratori.
Mooing Duck

5
std::iteratorè stato proposto di essere deprecato in C ++ 17 ; non lo era, ma non mi sarei mai fidato del fatto che restasse in giro ancora per molto.
einpoklum,

2
Un aggiornamento al commento di @ einpoklum: std::iteratordopo tutto è stato deprecato.
scry

1
@JonathanLee: Wow, operator boolè incredibilmente pericoloso. Qualcuno proverà a usarlo per rilevare la fine di un intervallo while(it++), ma tutto ciò che controlla realmente è se l'iteratore è stato costruito con un parametro.
Mooing Duck,

16

La documentazione iterator_facade di Boost.Iterator fornisce quello che sembra un bel tutorial sull'implementazione di iteratori per un elenco collegato. Potresti usarlo come punto di partenza per costruire un iteratore ad accesso casuale sul tuo contenitore?

Se non altro, puoi dare un'occhiata alle funzioni membro e ai typedef forniti da iterator_facadee usarlo come punto di partenza per costruire il tuo.



10

Ecco un esempio di iteratore puntatore non elaborato.

Non dovresti usare la classe iteratore per lavorare con puntatori non elaborati!

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

Soluzione alternativa basata su intervallo di puntatori non elaborati. Per favore, correggimi, se c'è un modo migliore per fare un ciclo basato sull'intervallo dal puntatore non elaborato.

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

E semplice test

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

5

Prima di tutto si può guardare qui per una lista delle varie operazioni i singoli tipi di iteratori hanno bisogno di sostegno.

Successivamente, quando hai creato la tua classe iteratore devi specializzarti std::iterator_traitsper essa e fornire alcuni typedefs necessari (come iterator_categoryo value_type) o in alternativa derivarne std::iterator, che definisce i typedefs necessari per te e può quindi essere utilizzato con l'impostazione predefinita std::iterator_traits.

disclaimer: so che ad alcune persone non piace cplusplus.commolto, ma forniscono alcune informazioni davvero utili al riguardo.


Davvero non capisco la disputa cplusplus vs cppreference, sono entrambi buoni e mancano molte cose. Tuttavia, C ++ è l'unico linguaggio in cui l'implementazione di iteratori di librerie standard è un inferno XD. La maggior parte delle volte è più semplice scrivere una classe wrapper su un contenitore stl che implementare un iteratore XD
CoffeDeveloper

@GameDeveloper controlla questa libreria di modelli che ho scritto per implementare gli iteratori: github.com/VinGarcia/Simple-Iterator-Template . È molto semplice e richiede solo circa 10 righe di codice per scrivere un iteratore.
VinGarcia,

Bella classe, lo apprezzo, probabilmente vale la pena portarlo per compilare anche con contenitori non STL (EA_STL, UE4) .. Consideralo! :)
CoffeDeveloper

Ad ogni modo, se l'unica ragione è che cplusplus.com fornisce alcune informazioni davvero utili, cppreference.com fornisce informazioni più utili ...
LF

@LF Quindi, sentiti libero di tornare indietro nel tempo e aggiungere tali informazioni alla versione 2011 del sito. ;-)
Christian Rau

3

Ero / sono nella tua stessa barca per diversi motivi (in parte educativi, in parte vincoli). Ho dovuto riscrivere tutti i contenitori della libreria standard e i contenitori dovevano essere conformi allo standard. Ciò significa che, se cambio il mio contenitore con la versione stl , il codice funzionerebbe allo stesso modo. Il che significava anche che dovevo riscrivere gli iteratori.

Comunque, ho guardato EASTL . Oltre a imparare un sacco di contenitori che non ho mai imparato tutto questo tempo usando i contenitori stl o attraverso i miei corsi di laurea. Il motivo principale è che EASTL è più leggibile rispetto alla controparte stl (ho scoperto che questo è semplicemente a causa della mancanza di tutte le macro e dello stile di codifica diretto). Ci sono alcune cose icky lì dentro (come #ifdefs per le eccezioni) ma niente che ti travolga.

Come altri hanno già detto, guarda il riferimento di cplusplus.com su iteratori e contenitori.


3

Stavo cercando di risolvere il problema di poter iterare su diversi array di testo, che sono tutti archiviati in un database residente in memoria di grandi dimensioni struct.

Di seguito è stato elaborato utilizzando Visual Studio 2017 Community Edition su un'applicazione di test MFC. Lo sto includendo come esempio in quanto questo post è stato uno dei tanti che ho incontrato che mi ha fornito un aiuto, ma che era ancora insufficiente per le mie esigenze.

Il structcontenuto dei dati residenti in memoria era simile al seguente. Ho rimosso la maggior parte degli elementi per brevità e non ho incluso nemmeno il preprocessore definito usato (l'SDK in uso è per C e C ++ ed è vecchio).

Quello che mi interessava fare era avere iteratori per i vari WCHARarray bidimensionali che contenevano stringhe di testo per la mnemonica.

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;

L'approccio attuale consiste nell'utilizzare un modello per definire una classe proxy per ciascuno degli array e quindi disporre di una singola classe iteratore che può essere utilizzata per iterare su un determinato array utilizzando un oggetto proxy che rappresenta l'array.

Una copia dei dati residenti in memoria è archiviata in un oggetto che gestisce la lettura e la scrittura dei dati residenti in memoria da / su disco. Questa classe, CFileParacontiene la classe proxy templato ( MnemonicIteratorDimSizee la classe secondaria da cui si deriva, MnemonicIteratorDimSizeBase) e la classe iteratore, MnemonicIterator.

L'oggetto proxy creato è collegato a un oggetto iteratore che accede alle informazioni necessarie tramite un'interfaccia descritta da una classe base da cui derivano tutte le classi proxy. Il risultato è avere un solo tipo di classe iteratore che può essere utilizzato con diverse classi proxy diverse poiché le diverse classi proxy espongono tutte la stessa interfaccia, l'interfaccia della classe base proxy.

La prima cosa era creare un insieme di identificatori che sarebbero stati forniti a una factory di classe per generare l'oggetto proxy specifico per quel tipo di mnemonico. Questi identificatori vengono utilizzati come parte dell'interfaccia utente per identificare i dati di provisioning specifici che l'utente è interessato a vedere e possibilmente modificare.

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 classe proxy

La classe proxy basata su modelli e la relativa classe base sono le seguenti. Avevo bisogno di ospitare diversi tipi di wchar_tarray di stringhe di testo. Le matrici bidimensionali avevano numeri diversi di mnemonici, a seconda del tipo (scopo) del mnemonico e i diversi tipi di mnemonici avevano lunghezze massime diverse, che variavano tra cinque caratteri di testo e venti caratteri di testo. I modelli per la classe proxy derivata si adattavano perfettamente al modello che richiedeva il numero massimo di caratteri in ciascun mnemonico. Dopo aver creato l'oggetto proxy, utilizziamo quindi il SetRange()metodo per specificare l'array mnemonico effettivo e il suo intervallo.

// 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 classe Iterator

La stessa classe iteratore è la seguente. Questa classe fornisce solo le funzionalità di base dell'iteratore avanzato che è tutto ciò che è necessario in questo momento. Tuttavia mi aspetto che questo cambi o venga esteso quando ho bisogno di qualcosa in più.

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 factory di oggetti proxy determina quale oggetto creare in base all'identificatore mnemonico. L'oggetto proxy viene creato e il puntatore restituito è il tipo di classe base standard in modo da avere un'interfaccia uniforme indipendentemente da quale delle diverse sezioni mnemoniche si accede. Il SetRange()metodo è utilizzato per specificare l'oggetto proxy elementi dell'array specifici proxy rappresenta e la gamma degli elementi dell'array.

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

Utilizzo della classe proxy e dell'iteratore

La classe proxy e il suo iteratore vengono utilizzati come mostrato nel ciclo seguente per compilare un CListCtrloggetto con un elenco di mnemonici. Sto usando in std::unique_ptrmodo che quando la classe proxy non è più necessaria e non std::unique_ptrrientra nell'ambito di applicazione, la memoria verrà ripulita.

Ciò che fa questo codice sorgente è creare un oggetto proxy per l'array all'interno del structquale corrisponde all'identificatore mnemonico specificato. Quindi crea un iteratore per quell'oggetto, usa un intervallo forper riempire il CListCtrlcontrollo e quindi pulisce. Queste sono tutte wchar_tstringhe di testo non elaborate che possono corrispondere esattamente al numero di elementi dell'array, pertanto copiamo la stringa in un buffer temporaneo al fine di garantire che il testo sia terminato con zero.

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

1

E ora un iteratore di chiavi per range-based per loop.

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
}

Questo è quello che stavo cercando. Ma nessuno ce l'aveva, a quanto pare.

Ottieni il mio allineamento del codice OCD come bonus.

Come esercizio, scrivi il tuo per values(my_map)

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.