web-dev-qa-db-fra.com

Comment implémenter un itérateur de style STL et éviter les pièges courants?

J'ai créé une collection pour laquelle je souhaite fournir un itérateur à accès aléatoire de style STL. Je cherchais un exemple d'implémentation d'un itérateur, mais je n'en ai trouvé aucun. Je connais le besoin de surcharges const des opérateurs [] et *. Quelles sont les conditions requises pour qu'un itérateur soit de "style STL" et quels sont les autres pièges à éviter (le cas échéant)?

Contexte supplémentaire: Ceci est pour une bibliothèque et je ne veux pas en introduire de dépendance sauf si j'en ai vraiment besoin. J'écris ma propre collection pour pouvoir fournir une compatibilité binaire entre C++ 03 et C++ 11 avec le même compilateur (donc pas de STL qui risquerait probablement de casser).

277
Tamás Szelei

http://www.cplusplus.com/reference/std/iterator/ a un tableau pratique qui détaille les spécifications du § 24.2.2 de la norme C++ 11. En gros, les itérateurs ont des balises décrivant les opérations valides et les balises ont une hiérarchie. Au-dessous est purement symbolique, ces classes n'existent pas en tant que telles.

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.

Vous pouvez soit spécialiser std::iterator_traits<youriterator>, soit mettre les mêmes corrections de type dans l'itérateur lui-même, soit hériter de std::iterator (qui possède ces corrections de type). Je préfère la deuxième option, pour éviter de changer les choses dans l'espace de noms std, et pour des raisons de lisibilité, mais la plupart des gens héritent 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
};

Notez que la catégorie iterator_category doit être l'une de std::input_iterator_tag, std::output_iterator_tag, std::forward_iterator_tag, std::bidirectional_iterator_tag ou std::random_access_iterator_tag, selon les exigences auxquelles votre itérateur satisfait. En fonction de votre itérateur, vous pouvez choisir de vous spécialiser std::next, std::prev, std::advance et std::distance, mais cela est rarement nécessaire. Dans les cas extrêmement rares , vous souhaiterez peut-être vous spécialiser std::begin et std::end.

Votre conteneur devrait probablement aussi avoir un const_iterator, qui est un itérateur (éventuellement modifiable) de données constantes et similaire à votre iterator sauf qu'il doit être implicitement constructible à partir d'un iterator et que les utilisateurs doivent être incapable de modifier les données. Il est courant que son pointeur interne soit un pointeur sur des données non constantes et qu'il hérite de iterator de const_iterator afin de minimiser la duplication de code.

Mon article sur Écriture de votre propre conteneur STL contient un prototype de conteneur/itérateur plus complet.

216
Mooing Duck

Le documentation iterator_facade de Boost.Iterator fournit ce qui ressemble à un tutoriel de Nice sur la mise en oeuvre d'itérateurs pour une liste chaînée. Pourriez-vous vous en servir comme point de départ pour créer un itérateur à accès aléatoire sur votre conteneur?

Si rien d'autre, vous pouvez jeter un coup d'œil aux fonctions membres et aux typedefs fournies par iterator_facade et les utiliser comme point de départ pour créer vos propres.

15

Thomas Becker a écrit un article utile sur le sujet ici .

Il y avait aussi cette approche (peut-être plus simple) qui était apparue auparavant sur SO: Comment implémenter correctement les itérateurs personnalisés et const_iterators?

9
Gnawme

Voici un exemple d'itérateur de pointeur brut.

Vous ne devriez pas utiliser la classe iterator pour travailler avec des pointeurs bruts!

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

Solution brute de boucle basée sur une plage de pointeurs bruts. S'il vous plaît, corrigez-moi, s'il existe un meilleur moyen de créer une boucle basée sur une plage à partir d'un pointeur brut.

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

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

Tout d’abord, vous pouvez consulter ici pour obtenir une liste des différentes opérations que chaque type d’itérateur doit prendre en charge.

Ensuite, lorsque vous avez créé votre classe d'itérateur, vous devez soit vous spécialiser std::iterator_traits et lui fournir quelques typedefs (comme _iterator_category_ ou _value_type_), ou bien dériver à partir de std::iterator , qui définit le nom typedefs nécessaire et peut donc être utilisé avec la valeur par défaut _std::iterator_traits_.

disclaimer: Je sais que certaines personnes n'aiment pas beaucoup _cplusplus.com_, mais elles fournissent des informations très utiles à ce sujet.

4
Christian Rau

J'étais/suis dans le même bateau que vous pour différentes raisons (en partie éducatives, en partie contraintes). Je devais réécrire tous les conteneurs de la bibliothèque standard et les conteneurs devaient être conformes à la norme. Cela signifie que si je permutais mon conteneur avec la version stl , le code fonctionnerait de la même manière. Ce qui signifiait également que je devais réécrire les itérateurs.

Quoi qu'il en soit, j'ai regardé EASTL . En dehors d’apprendre énormément sur les conteneurs que je n’ai jamais appris tout ce temps en utilisant les conteneurs stl ou dans mes cours de premier cycle. La raison principale est que EASTL est plus lisible que le stl contrepartie (j’ai trouvé cela simplement à cause de l’absence de toutes les macros et d’un style de codage simple). Il y a des choses épineuses dedans (comme #ifdefs pour les exceptions) mais rien ne vous submerge.

Comme d'autres l'ont mentionné, consultez la référence de cplusplus.com sur les itérateurs et les conteneurs.

2
Samaursa

J'essayais de résoudre le problème de la possibilité d'effectuer une itération sur plusieurs tableaux de texte différents, qui sont tous stockés dans une base de données résidente en mémoire qui est une grande struct.

Les éléments suivants ont été élaborés à l'aide de Visual Studio 2017 Community Edition sur une application de test MFC. J'inclue ceci à titre d'exemple, car cette publication était l'une des nombreuses que j'ai rencontrées et qui fournissait une aide mais restaient insuffisantes pour mes besoins.

La struct contenant les données résidentes en mémoire ressemblait à ce qui suit. J'ai supprimé la plupart des éléments par souci de brièveté et n'ai pas non plus inclus les définitions de préprocesseur utilisées (le SDK utilisé est à la fois pour C et pour C++ et est ancien).

Ce qui m'intéressait, c’est d’avoir des itérateurs pour les divers tableaux WCHAR bidimensionnels qui contenaient des chaînes de texte pour les mnémoniques.

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'approche actuelle consiste à utiliser un modèle pour définir une classe proxy pour chacun des tableaux, puis à avoir une seule classe itérateur pouvant être utilisée pour itérer sur un tableau particulier à l'aide d'un objet proxy représentant le tableau.

Une copie des données résidentes en mémoire est stockée dans un objet qui gère la lecture et l'écriture des données résidentes en mémoire à partir du/sur le disque. Cette classe, CFilePara contient la classe proxy basée sur un modèle (MnemonicIteratorDimSize et la sous-classe dont elle est issue, MnemonicIteratorDimSizeBase) et la classe itérateur, MnemonicIterator.

L'objet proxy créé est attaché à un objet itérateur qui accède aux informations nécessaires via une interface décrite par une classe de base à partir de laquelle toutes les classes proxy sont dérivées. Le résultat est d'avoir un seul type de classe d'itérateur pouvant être utilisé avec plusieurs classes de proxy car les différentes classes de proxy présentent toutes la même interface, l'interface de la classe de base du proxy.

La première chose à faire était de créer un ensemble d’identifiants qui seraient fournis à une fabrique de classes afin de générer l’objet proxy spécifique pour ce type de mnémonique. Ces identifiants sont utilisés dans le cadre de l'interface utilisateur pour identifier les données d'approvisionnement spécifiques que l'utilisateur est intéressé à voir et éventuellement à modifier.

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

La classe proxy basée sur un modèle et sa classe de base sont les suivantes. Je devais prendre en charge plusieurs types différents de tableaux de chaînes de texte wchar_t. Les tableaux bidimensionnels avaient des nombres de mnémoniques différents, en fonction du type (but) de la mnémonique et les différents types de mnémoniques avaient des longueurs maximales différentes, variant entre cinq caractères de texte et vingt caractères de texte. Les modèles pour la classe proxy dérivée correspondaient parfaitement au modèle nécessitant le nombre maximal de caractères dans chaque mnémonique. Une fois l’objet proxy créé, nous utilisons ensuite la méthode SetRange() pour spécifier le tableau mnémonique actuel et sa plage.

// 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 Itérateur

La classe itérateur elle-même est la suivante. Cette classe fournit uniquement une fonctionnalité de base d'itérateur de transfert, qui est tout ce dont vous avez besoin pour le moment. Cependant, je pense que cela va changer ou s’étendre lorsque j’ai besoin de quelque chose de plus.

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 fabrique d’objets proxy détermine l’objet à créer en fonction de l’identifiant mnémonique. L'objet proxy est créé et le pointeur renvoyé est le type de classe de base standard afin de disposer d'une interface uniforme, quelle que soit la section d'accès mnémonique utilisée. La méthode SetRange() permet de spécifier à l'objet proxy les éléments de tableau spécifiques représentés par le proxy et la plage des éléments de tableau.

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

tilisation de la classe de proxy et de l'itérateur

La classe proxy et son itérateur sont utilisés comme indiqué dans la boucle suivante pour renseigner un objet CListCtrl avec une liste de mnémoniques. J'utilise std::unique_ptr de sorte que, lorsque la classe de proxy n'est plus utilisée et que le std::unique_ptr ne soit plus concerné, la mémoire est nettoyée.

Ce code source crée un objet proxy pour le tableau dans la variable struct qui correspond à l'identificateur mnémonique spécifié. Il crée ensuite un itérateur pour cet objet, utilise une plage for pour renseigner le contrôle CListCtrl, puis nettoie. Ce sont toutes des chaînes de texte brutes wchar_t qui peuvent correspondre exactement au nombre d'éléments du tableau. Nous copions donc la chaîne dans un tampon temporaire afin de nous assurer que le texte est à zéro.

    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
Richard Chambers