it-swarm.com.de

Wie implementiere ich einen Iterator im STL-Stil und vermeide häufige Fallstricke?

Ich habe eine Sammlung erstellt, für die ich einen Iterator mit wahlfreiem Zugriff im STL-Stil bereitstellen möchte. Ich habe nach einer Beispielimplementierung eines Iterators gesucht, aber keine gefunden. Ich weiß um die Notwendigkeit von const-Überladungen von [] und * Operatoren. Was sind die Anforderungen an einen Iterator im STL-Stil und welche anderen Fallstricke sind zu vermeiden (falls vorhanden)?

Zusätzlicher Kontext: Dies ist für eine Bibliothek und ich möchte keine Abhängigkeit davon einführen, es sei denn, ich muss es wirklich tun. Ich schreibe meine eigene Sammlung, um die Binärkompatibilität zwischen C++ 03 und C++ 11 mit demselben Compiler zu gewährleisten (also keine STL, die wahrscheinlich kaputt gehen würde).

277
Tamás Szelei

http://www.cplusplus.com/reference/std/iterator/ enthält eine handliche Tabelle mit den Spezifikationen von § 24.2.2 des C++ 11-Standards. Grundsätzlich haben die Iteratoren Tags, die die gültigen Operationen beschreiben, und die Tags haben eine Hierarchie. Das Folgende ist rein symbolisch, diese Klassen existieren eigentlich nicht als solche.

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.

Sie können entweder std::iterator_traits<youriterator> spezialisieren oder dieselben Typedefs in den Iterator selbst einfügen oder von std::iterator (der diese Typedefs hat) erben. Ich bevorzuge die zweite Option, um Änderungen im std -Namensraum zu vermeiden und um die Lesbarkeit zu gewährleisten, aber die meisten Leute erben von 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
};

Beachten Sie, dass die Iteratorkategorie entweder std::input_iterator_tag, std::output_iterator_tag, std::forward_iterator_tag, std::bidirectional_iterator_tag oder std::random_access_iterator_tag sein muss, je nachdem, welche Anforderungen Ihr Iterator erfüllt. Abhängig von Ihrem Iterator können Sie sich auch für die Spezialisierung von std::next, std::prev, std::advance und std::distance entscheiden, dies ist jedoch selten erforderlich. In äußerst seltenen Fällen möchten Sie möglicherweise std::begin und std::end spezialisieren.

Ihr Container sollte wahrscheinlich auch einen const_iterator haben, der ein (möglicherweise veränderbarer) Iterator für konstante Daten ist, der Ihrem iterator ähnelt, mit der Ausnahme, dass er implizit aus einem iterator konstruiert werden kann und Benutzer nicht in der Lage sein sollten, Änderungen vorzunehmen die Daten. Es ist üblich, dass sein interner Zeiger ein Zeiger auf nicht konstante Daten ist und iterator von const_iterator erbt, um die Codeduplizierung zu minimieren.

Mein Beitrag unter Schreiben eines eigenen STL-Containers enthält einen vollständigeren Container/Iterator-Prototyp.

216
Mooing Duck

Die iterator_facade documentation von Boost.Iterator enthält ein nützliches Tutorial zur Implementierung von Iteratoren für eine verknüpfte Liste. Könnten Sie das als Ausgangspunkt für die Erstellung eines Iterators mit wahlfreiem Zugriff über Ihren Container verwenden?

Wenn nicht anders angegeben, können Sie sich die Memberfunktionen und typedefs ansehen, die von iterator_facade Bereitgestellt werden, und sie als Ausgangspunkt für die Erstellung Ihrer eigenen verwenden.

15

Thomas Becker hat einen nützlichen Artikel zum Thema geschrieben hier .

Es gab auch diesen (vielleicht einfacheren) Ansatz, der zuvor auf SO: Wie werden benutzerdefinierte Iteratoren und const_iterators korrekt implementiert?

9
Gnawme

Hier ist ein Beispiel eines rohen Zeiger-Iterators.

Sie sollten die Iteratorklasse nicht verwenden, um mit rohen Zeigern zu arbeiten!

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

Problemumgehung für eine auf dem Bereich des Rohzeigers basierende Schleife. Bitte korrigieren Sie mich, wenn es eine bessere Möglichkeit gibt, eine bereichsbasierte Schleife aus einem rohen Zeiger zu erstellen.

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

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

Zunächst können Sie hier nach einer Liste der verschiedenen Operationen suchen, die die einzelnen Iteratortypen unterstützen müssen.

Wenn Sie Ihre Iteratorklasse erstellt haben, müssen Sie entweder std::iterator_traits spezialisieren und einige notwendige typedefs (wie iterator_category oder value_type) oder leiten Sie es alternativ von std::iterator ab, das die benötigten typedefs für Sie definiert und daher mit verwendet werden kann der Standardwert std::iterator_traits.

Haftungsausschluss: Ich weiß, einige Leute mögen cplusplus.com nicht so sehr, aber sie liefern einige wirklich nützliche Informationen dazu.

4
Christian Rau

Ich war/bin aus verschiedenen Gründen (teils lehrreich, teils eingeschränkt) im selben Boot wie Sie. Ich musste alle Container der Standardbibliothek neu schreiben und die Container mussten dem Standard entsprechen. Das heißt, wenn ich meinen Container mit der stl -Version austausche, funktioniert der Code genauso. Das bedeutete auch, dass ich die Iteratoren neu schreiben musste.

Jedenfalls habe ich mir EASTL angesehen. Abgesehen davon, dass ich eine Menge über Container gelernt habe, die ich die ganze Zeit nicht mit den stl -Containern oder durch meine Grundstudiengänge gelernt habe. Der Hauptgrund ist, dass [~ # ~] eastl [~ # ~] besser lesbar ist als die stl Gegenstück (ich fand dies einfach wegen des Fehlens aller Makros und des einfachen Codierungsstils). Es gibt einige verdammte Dinge (wie #ifdefs für Ausnahmen), aber nichts, was Sie überwältigen könnte.

Schauen Sie sich, wie bereits erwähnt, die Referenz von cplusplus.com zu Iteratoren und Containern an.

2
Samaursa

Ich habe versucht, das Problem zu lösen, dass mehrere verschiedene Textfelder durchlaufen werden können, die alle in einer speicherresidenten Datenbank gespeichert sind, die eine große struct ist.

Das Folgende wurde mit Visual Studio 2017 Community Edition in einer MFC-Testanwendung ausgearbeitet. Ich füge dies als Beispiel hinzu, da es sich bei diesem Beitrag um einen von mehreren handelt, die mir über den Weg gelaufen sind und die mir geholfen haben, aber für meine Bedürfnisse immer noch nicht ausreichen.

Das struct mit den speicherresidenten Daten sah ungefähr so ​​aus. Ich habe der Kürze halber die meisten Elemente entfernt und auch die verwendeten Präprozessor-Definitionen nicht berücksichtigt (das SDK wird sowohl für C als auch für C++ verwendet und ist alt).

Was mich interessiert hat, sind Iteratoren für die verschiedenen WCHAR zweidimensionalen Arrays, die Textstrings für Mnemonics enthalten.

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;

Der aktuelle Ansatz besteht darin, eine Vorlage zu verwenden, um eine Proxy-Klasse für jedes der Arrays zu definieren, und dann eine einzelne Iterator-Klasse zu haben, die verwendet werden kann, um über ein bestimmtes Array zu iterieren, indem ein Proxy-Objekt verwendet wird, das das Array darstellt.

Eine Kopie der speicherresidenten Daten wird in einem Objekt gespeichert, das das Lesen und Schreiben der speicherresidenten Daten von/auf die Festplatte handhabt. Diese Klasse CFilePara enthält die Proxy-Klasse mit Vorlagen (MnemonicIteratorDimSize und die Unterklasse, von der sie abgeleitet ist, MnemonicIteratorDimSizeBase) und die Iteratorklasse MnemonicIterator. .

Das erstellte Proxy-Objekt wird an ein Iterator-Objekt angehängt, das über eine Schnittstelle, die von einer Basisklasse beschrieben wird, von der alle Proxy-Klassen abgeleitet sind, auf die erforderlichen Informationen zugreift. Das Ergebnis ist ein einziger Typ von Iteratorklasse, der mit mehreren verschiedenen Proxy-Klassen verwendet werden kann, da die verschiedenen Proxy-Klassen alle dieselbe Schnittstelle, die Schnittstelle der Proxy-Basisklasse, offenlegen.

Das erste war, eine Reihe von Bezeichnern zu erstellen, die einer Klassenfactory zur Verfügung gestellt wurden, um das spezifische Proxy-Objekt für diesen Mnemoniktyp zu generieren. Diese Kennungen werden als Teil der Benutzeroberfläche verwendet, um die bestimmten Bereitstellungsdaten zu identifizieren, die der Benutzer sehen und möglicherweise ändern möchte.

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;

Die Proxy-Klasse

Die Proxy-Klasse mit Vorlagen und ihre Basisklasse lauten wie folgt. Ich musste verschiedene Arten von wchar_t - Text-String-Arrays aufnehmen. Die zweidimensionalen Arrays hatten je nach Art (Zweck) der Mnemonik eine unterschiedliche Anzahl von Mnemoniken, und die verschiedenen Arten von Mnemoniken hatten eine unterschiedliche maximale Länge und variierten zwischen fünf Textzeichen und zwanzig Textzeichen. Vorlagen für die abgeleitete Proxy-Klasse passten auf natürliche Weise zu der Vorlage, für die die maximale Anzahl von Zeichen in jeder Mnemonik erforderlich war. Nachdem das Proxy-Objekt erstellt wurde, verwenden wir die SetRange() -Methode, um das tatsächliche mnemonische Array und seinen Bereich anzugeben.

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

};

The Iterator Class

Die Iteratorklasse selbst lautet wie folgt. Diese Klasse bietet nur grundlegende Forward-Iterator-Funktionen, die zu diesem Zeitpunkt nur benötigt werden. Ich gehe jedoch davon aus, dass sich dies ändern oder erweitern wird, wenn ich etwas zusätzliches benötige.

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

Die Proxy-Objekt-Factory bestimmt anhand der mnemonischen Kennung, welches Objekt erstellt werden soll. Das Proxy-Objekt wird erstellt und der zurückgegebene Zeiger ist der Standardtyp der Basisklasse, sodass unabhängig davon, auf welche der verschiedenen Mnemonic-Abschnitte zugegriffen wird, eine einheitliche Schnittstelle vorhanden ist. Die SetRange() -Methode wird verwendet, um dem Proxy-Objekt die spezifischen Array-Elemente, die der Proxy darstellt, und den Bereich der Array-Elemente anzugeben.

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

Verwenden der Proxy-Klasse und des Iterators

Die Proxy-Klasse und ihr Iterator werden wie in der folgenden Schleife gezeigt verwendet, um ein CListCtrl -Objekt mit einer Liste von Mnemonics auszufüllen. Ich verwende std::unique_ptr, Damit der Speicher aufgeräumt wird, wenn die Proxy-Klasse nicht mehr benötigt wird und std::unique_ptr Nicht mehr gültig ist.

Mit diesem Quellcode wird ein Proxy-Objekt für das Array innerhalb von struct erstellt, das dem angegebenen mnemonischen Bezeichner entspricht. Es erstellt dann einen Iterator für dieses Objekt, füllt das Steuerelement for mithilfe eines Bereichs CListCtrl aus und räumt anschließend auf. Dies sind alles rohe wchar_t Textzeichenfolgen, die genau die Anzahl der Array-Elemente sein können. Daher kopieren wir die Zeichenfolge in einen temporären Puffer, um sicherzustellen, dass der Text mit Null abgeschlossen ist.

    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