Como implementar um iterador no estilo STL e evitar armadilhas comuns?

306

Fiz uma coleção para a qual desejo fornecer um iterador de acesso aleatório no estilo STL. Eu estava procurando um exemplo de implementação de um iterador, mas não encontrei nenhum. Eu sei sobre a necessidade de sobrecargas const []e *operadores. Quais são os requisitos para um iterador ser "estilo STL" e quais outras armadilhas devem ser evitadas (se houver)?

Contexto adicional: é para uma biblioteca e não quero introduzir nenhuma dependência a menos que realmente precise. Eu escrevo minha própria coleção para poder fornecer compatibilidade binária entre C ++ 03 e C ++ 11 com o mesmo compilador (portanto, nenhum STL provavelmente quebraria).

Tamás Szelei
fonte
13
+1! Boa pergunta. Eu me perguntei a mesma coisa. É fácil o suficiente para alternar algo com base no Boost.Iterator, mas é surpreendentemente difícil encontrar uma lista dos requisitos se você implementá-lo do zero.
jalf
2
Lembre-se também de que seus iteradores precisam ser ASSUSTADORES. boost.org/doc/libs/1_55_0/doc/html/intrusive/…
alfC

Respostas:

232

http://www.cplusplus.com/reference/std/iterator/ possui um gráfico útil que detalha as especificações do § 24.2.2 do padrão C ++ 11. Basicamente, os iteradores têm tags que descrevem as operações válidas e as tags possuem uma hierarquia. Abaixo, é puramente simbólico, essas classes não existem realmente como tais.

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.

Você pode se especializar std::iterator_traits<youriterator>ou colocar os mesmos typedefs no próprio iterador ou herdar std::iterator(que possui esses typedefs). Prefiro a segunda opção, para evitar alterar as coisas no stdespaço para nome e para facilitar a leitura, mas a maioria das pessoas herda 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
};

Observe o iterator_category deve ser um dos std::input_iterator_tag, std::output_iterator_tag, std::forward_iterator_tag, std::bidirectional_iterator_tag, ou std::random_access_iterator_tag, dependendo de quais requisitos seus iteradoras satisfaz. Dependendo do seu iterador, você pode optar por se especializar std::next, std::prev, std::advance, e std::distance, bem como, mas isso raramente é necessário. Em casos extremamente raros , você pode querer se especializar std::begine std::end.

Seu contêiner provavelmente também deve ter um const_iteratoriterador (possivelmente mutável) para dados constantes semelhantes ao seu, iteratorexceto que ele deve ser implicitamente construtível a partir de ae os iteratorusuários não devem poder modificar os dados. É comum que seu ponteiro interno seja um ponteiro para dados não constantes e tenha iteratorherdado de const_iteratormodo a minimizar a duplicação de código.

Minha postagem em Writing your own STL Container possui um protótipo de contêiner / iterador mais completo.

Mooing Duck
fonte
2
Além de se especializar std::iterator_traitsou definir os typedefs, você também pode derivar de std::iterator, que os define, dependendo dos parâmetros do modelo.
Christian Rau
3
@LokiAstari: A documentação completa é bastante extensa (40 páginas no rascunho) e não está no escopo do Stack Overflow. No entanto, adicionei mais informações detalhando as tags e iterador const_iterator. O que mais estava faltando no meu post? Você parece sugerir que há mais para adicionar à classe, mas a pergunta é especificamente sobre a implementação de iteradores.
Mooing Duck
5
std::iteratorfoi proposto como obsoleto em C ++ 17 ; não era, mas eu não acreditaria nisso por muito mais tempo.
Einpoklum
2
Uma atualização ao comentário de @ einpoklum: std::iteratorfoi descontinuada.
scry
1
@ JonathanLee: Uau, isso operator boolé incrivelmente perigoso. Alguém tentará usar isso para detectar o final de um intervalo while(it++), mas tudo o que realmente verifica é se o iterador foi construído com um parâmetro.
Mooing Duck
16

A documentação iterator_facade do Boost.Iterator fornece o que parece ser um bom tutorial sobre como implementar iteradores para uma lista vinculada. Você poderia usar isso como ponto de partida para criar um iterador de acesso aleatório sobre seu contêiner?

Se nada mais, você pode dar uma olhada nas funções-membro e nos typedefs fornecidos por iterator_facadee usá-los como ponto de partida para criar seus próprios.

Michael Kristofik
fonte
10

Aqui está uma amostra do iterador de ponteiro bruto.

Você não deve usar a classe iterator para trabalhar com ponteiros brutos!

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

Solução de loop com base no intervalo do ponteiro bruto. Por favor, corrija-me, se houver uma maneira melhor de fazer um loop baseado em intervalo a partir do ponteiro bruto.

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 teste simples

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
fonte
5

Primeiro, você pode procurar aqui uma lista das várias operações que os tipos de iteradores individuais precisam oferecer suporte.

Em seguida, quando você tiver criado sua classe de iterador, precisará se especializar std::iterator_traitse fornecer alguns typedefs necessários (como iterator_categoryou value_type) ou derivá-lo alternativamente std::iterator, que define os typedefs necessários para você e, portanto, pode ser usado com o padrão std::iterator_traits.

isenção de responsabilidade: sei que algumas pessoas não gostam cplusplus.commuito, mas fornecem algumas informações realmente úteis sobre isso.

Christian Rau
fonte
Eu realmente não entendo a disputa cplusplus vs cppreference, elas são boas e faltam muitas coisas. No entanto, C ++ é a única linguagem em que a implementação de iteradores de biblioteca padrão é um XD infernal. Na maioria das vezes é mais simples de escrever uma classe wrapper sobre um recipiente STL do que a implementação de um XD iterador
CoffeDeveloper
@GameDeveloper verifique esta biblioteca de modelos que escrevi para implementar iteradores: github.com/VinGarcia/Simple-Iterator-Template . É muito simples e requer apenas 10 linhas de código para escrever um iterador.
VinGarcia #
Boa aula, eu agradeço, provavelmente vale a pena portá-la para compilar também com contêineres não STL (EA_STL, UE4) .. Considere! :)
CoffeDeveloper
Enfim, se a única razão é que cplusplus.com fornece alguma informação realmente útil, cppreference.com fornece informações mais mais útil ...
LF
@LF Então, sinta-se à vontade para voltar no tempo e adicionar essas informações à versão 2011 do site. ;-)
Christian Rau
3

Eu estava no mesmo barco que você por diferentes razões (em parte educacionais, em parte restrições). Eu tive que reescrever todos os contêineres da biblioteca padrão e os contêineres tiveram que estar em conformidade com o padrão. Isso significa que, se eu trocar meu contêiner pela versão stl , o código funcionará da mesma maneira. O que também significava que eu tinha que reescrever os iteradores.

Enfim, eu olhei para o EASTL . Além de aprender muito sobre contêineres que eu nunca aprendi esse tempo todo usando os contêineres stl ou durante meus cursos de graduação. A principal razão é que o EASTL é mais legível do que o equivalente do stl (eu achei isso simplesmente devido à falta de todas as macros e ao estilo de codificação direto). Existem algumas coisas nojentas (como #ifdefs para exceções), mas nada para sobrecarregá-lo.

Como outros mencionados, consulte a referência da cplusplus.com sobre iteradores e contêineres.

Samaursa
fonte
3

Eu estava tentando resolver o problema de conseguir iterar em várias matrizes de texto diferentes, todas armazenadas em um banco de dados residente na memória que é grande struct.

O seguinte foi elaborado usando o Visual Studio 2017 Community Edition em um aplicativo de teste do MFC. Estou incluindo isso como um exemplo, pois essa postagem foi uma das várias que encontrei, fornecendo alguma ajuda, mas ainda assim insuficiente para minhas necessidades.

O structcontendo os dados residentes da memória era semelhante ao seguinte. Eu removi a maioria dos elementos por questões de brevidade e também não incluí as definições de pré-processador usadas (o SDK em uso é para C e C ++ e é antigo).

O que eu estava interessado em fazer é ter iteradores para as várias WCHARmatrizes bidimensionais que continham cadeias 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;

A abordagem atual é usar um modelo para definir uma classe de proxy para cada uma das matrizes e, em seguida, ter uma única classe de iterador que pode ser usada para iterar sobre uma matriz específica usando um objeto proxy que representa a matriz.

Uma cópia dos dados residentes da memória é armazenada em um objeto que lida com a leitura e gravação dos dados residentes da memória de / para o disco. Esta classe CFileParacontém a classe de proxy com modelo ( MnemonicIteratorDimSizee a subclasse da qual é derivada MnemonicIteratorDimSizeBase) e a classe do iterador MnemonicIterator,.

O objeto proxy criado é anexado a um objeto iterador que acessa as informações necessárias por meio de uma interface descrita por uma classe base da qual todas as classes proxy são derivadas. O resultado é ter um único tipo de classe de iterador que pode ser usado com várias classes de proxy diferentes, porque as diferentes classes de proxy expõem a mesma interface, a interface da classe base do proxy.

A primeira coisa foi criar um conjunto de identificadores que seriam fornecidos a uma fábrica de classes para gerar o objeto proxy específico para esse tipo de mnemônico. Esses identificadores são usados ​​como parte da interface do usuário para identificar os dados de provisionamento específicos que o usuário está interessado em ver e possivelmente 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;

A Classe Proxy

A classe de proxy modelada e sua classe base são as seguintes. Eu precisava acomodar vários tipos diferentes de wchar_tmatrizes de string de texto. As matrizes bidimensionais tinham números diferentes de mnemônicos, dependendo do tipo (objetivo) do mnemônico e os diferentes tipos de mnemônicos tinham diferentes comprimentos máximos, variando entre cinco caracteres e vinte caracteres. Os modelos para a classe de proxy derivada eram um ajuste natural ao modelo que requer o número máximo de caracteres em cada mnemônico. Após a criação do objeto proxy, usamos o SetRange()método para especificar a matriz mnemônica real e seu intervalo.

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

};

A classe Iterator

A própria classe de iterador é a seguinte. Essa classe fornece apenas a funcionalidade básica do iterador direto, que é tudo o que é necessário no momento. No entanto, espero que isso mude ou seja estendido quando precisar de 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
};

A fábrica de objetos proxy determina qual objeto será criado com base no identificador mnemônico. O objeto proxy é criado e o ponteiro retornado é o tipo de classe base padrão para ter uma interface uniforme, independentemente de quais das diferentes seções mnemônicas estão sendo acessadas. O SetRange()método é usado para especificar para o objeto proxy os elementos específicos da matriz que o proxy representa e o intervalo dos elementos da 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;
}

Usando a classe de proxy e o iterador

A classe proxy e seu iterador são usados ​​como mostrado no loop a seguir para preencher um CListCtrlobjeto com uma lista de mnemônicos. Estou usando std::unique_ptrpara que, quando a classe proxy não seja mais necessária e a std::unique_ptrsaída do escopo, a memória seja limpa.

O que esse código-fonte faz é criar um objeto proxy para a matriz dentro do structque corresponde ao identificador mnemônico especificado. Em seguida, ele cria um iterador para esse objeto, usa um intervalo forpara preencher o CListCtrlcontrole e depois limpa. Essas são todas as wchar_tstrings de texto bruto que podem ser exatamente o número de elementos da matriz, por isso copiamos a string em um buffer temporário para garantir que o texto seja zero terminado.

    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
fonte
1

E agora um iterador de chaves para loop for baseado em intervalo.

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
}

Era isso que eu estava procurando. Mas ninguém teve, ao que parece.

Você recebe meu alinhamento do código do TOC como um bônus.

Como exercício, escreva o seu para values(my_map)

Gabriel
fonte