STLスタイルのイテレーターを実装し、一般的な落とし穴を回避する方法は?


306

STLスタイルのランダムアクセスイテレータを提供するコレクションを作成しました。イテレータの実装例を探していましたが、見つかりませんでした。[]and *演算子のconstオーバーロードの必要性について知っています。イテレータが「STLスタイル」であるための要件は何ですか?また、(もしあれば)回避すべきその他の落とし穴は何ですか?

追加のコンテキスト:これはライブラリ用であり、本当に必要でない限り、ライブラリへの依存関係を導入したくありません。同じコンパイラでC ++ 03とC ++ 11のバイナリ互換性を提供できるように、独自のコレクションを作成します(したがって、おそらく壊れるSTLはありません)。


13
+1!いい質問です 私は同じことを思った。Boost.Iteratorに基づいて何かを一緒にフリックするのは簡単ですが、ゼロから実装する場合、要件のリストを見つけるだけでは驚くほど困難です。
2011年

2
イテレータは怖いものでなければならないことも覚えておいてください。boost.org/doc/libs/1_55_0/doc/html/intrusive/...
alfC

回答:


232

http://www.cplusplus.com/reference/std/iterator/には、C ++ 11標準の§24.2.2の仕様の詳細を示す便利な表があります。基本的に、イテレータには有効な操作を説明するタグがあり、タグには階層があります。以下は純粋にシンボリックですが、これらのクラスは実際には存在しません。

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.

を特殊化するかstd::iterator_traits<youriterator>、同じtypedefをイテレータ自体に配置するか、std::iterator(これらのtypedefを持つ)から継承することができます。std名前空間の変更を避け、読みやすくするために、2番目のオプションを好みますが、ほとんどの人はから継承し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
};

iterator_categoryのいずれかでなければなりません注意してくださいstd::input_iterator_tagstd::output_iterator_tagstd::forward_iterator_tagstd::bidirectional_iterator_tag、またはstd::random_access_iterator_tag、その要件あなたのイテレータを満たすに依存。あなたのイテレータによっては、特化する選択することができstd::nextstd::prevstd::advance、とstd::distanceだけでなく、これはほとんど必要ありません。非常にまれなケースあなたが専門することを望むかもしれないstd::beginstd::end

コンテナにはもあるはずですconst_iterator。これは、iteratorから暗黙的に構築可能でありiterator、ユーザーがデータを変更できないことを除いて、と同様の定数データへの(おそらく変更可能な)イテレータです。その内部ポインタが非定数データへのポインタであることが一般的であり、コードの重複を最小限にするためにiterator継承しconst_iteratorています。

独自のSTLコンテナーの作成に関する私の投稿には、より完全なコンテナー/反復子プロトタイプがあります。


2
std::iterator_traitstypedefを自分で特殊化または定義することに加えてstd::iterator、テンプレートパラメータに応じて、それらを定義するから単に派生させることもできます。
クリスチャンラウ

3
@LokiAstari:完全なドキュメントは非常に広範で(ドラフトの40ishページ)、Stack Overflowの範囲ではありません。ただし、イテレータタグとの詳細を追加しましたconst_iterator。私の投稿には他に何が欠けていましたか?クラスに追加する必要があることを示唆しているようですが、問題は特にイテレータの実装についてです。
Mooing Duck

5
std::iteratorC ++ 17で廃止されること提案されました。そうではありませんでしたが、私はそれがずっと長く存在することを期待していません。
アインポクルム2017

2
@einpoklumのコメントの更新:std::iterator結局非推奨になりました。
2018年

1
@JonathanLee:うわー、それoperator boolは非常に危険です。誰かがそれを使用して範囲の終わりを検出しようとしますがwhile(it++)、実際にチェックするのは、イテレータがパラメータ付きで作成されたかどうかだけです。
Mooing Duck 2018

16

Boost.Iterator のiterator_facadeドキュメンテーションは、リンクされたリストの反復子の実装に関する素晴らしいチュートリアルのように見えます。コンテナ上でランダムアクセスイテレータを作成するための出発点としてそれを使用できますか?

他に何もない場合は、によって提供されるメンバー関数とtypedefを見て、iterator_facadeそれを独自のメンバーを構築するための開始点として使用できます。



10

これが生のポインタ反復子のサンプルです。

生のポインタを操作するためにイテレータクラスを使用しないでください。

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

生のポインタ範囲ベースのループの回避策。生のポインタから範囲ベースのループを作成するより良い方法がある場合は、私を修正してください。

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

そして簡単なテスト

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

まず最初に、個々の反復子タイプがサポートする必要のあるさまざまな操作のリストをここで見ることができます。

次に、イテレータクラスを作成したら、そのクラスに特化std::iterator_traitsして必要なを提供するかtypedefiterator_categoryまたはなどvalue_type)、またはから派生させるstd::iterator必要typedefがありますstd::iterator_traits。これにより、必要なが定義され、デフォルトで使用できます。

免責事項:私は一部の人々がそれほど好きcplusplus.comではないことを知っていますが、これについていくつかの本当に役立つ情報を提供しています。


私はcplusplus対cppreferenceの論争を本当に受けていません、それらは両方とも良いことであり、多くのことを欠いています。ただし、C ++は、標準ライブラリイテレータの実装が地獄のXDである唯一の言語です。ほとんどの場合、イテレータXDを実装するよりもstlコンテナでラッパークラスを記述する方が簡単です
CoffeDeveloper '14 / 07/15

@GameDeveloperは、反復子を実装するために私が作成したこのテンプレートライブラリを確認します:github.com/VinGarcia/Simple-Iterator-Template。非常にシンプルで、イテレータを作成するのに必要なコードは約10行です。
VinGarcia 2017

素敵なクラスです。感謝します。非STLコンテナ(EA_STL、UE4)でもコンパイルできるように移植する価値はあります。考えてみてください。:)
CoffeDeveloper 2017

とにかく、cplusplus.comが本当に役立つ情報を提供することが唯一の理由である場合、cppreference.comはより有用な情報を提供します...
LF

@LF次に、時間を遡って、その情報をサイトの2011バージョンに追加してください。;-)
Christian Rau

3

さまざまな理由で(一部は教育、一部は制約)、私はあなたと同じ船に乗っていました。標準ライブラリのすべてのコンテナを書き直す必要があり、コンテナは標準に準拠する必要がありました。つまり、コンテナーをstlバージョンと入れ替えても、コードは同じように機能します。つまり、イテレータを書き直す必要がありました。

とにかくEASTLを見ました。stlコンテナーを使用して、または学部のコースを通じて、これまでに一度も学習したことがないコンテナーについてたくさん学ぶことは別として。主な理由は、ということですEASTLがより読みやすいのSTL(私はこれは単にため、すべてのマクロの欠如であり、まっすぐコーディングスタイル見つかっ)対応。そこにはいくつかの気の利いたもの(例外の#ifdefsなど)がありますが、あなたを圧倒するものは何もありません。

他の人が述べたように、イテレータとコンテナに関するcplusplus.comのリファレンスを見てください。


3

私は、いくつかの異なるテキスト配列を反復処理できるという問題を解決しようとしていました。それらはすべて、大規模なメモリ常駐データベース内に格納されていますstruct

以下は、MFCテストアプリケーションでVisual Studio 2017 Community Editionを使用して作成されました。この投稿は、私が遭遇したいくつかのヘルプの1つであるため、例として含めています。

struct含むメモリ常駐のデータは次のようなものが見えました。簡潔にするために、ほとんどの要素を削除し、使用したプリプロセッサ定義も含めていません(使用中のSDKはCおよびC ++向けであり、古いものです)。

私が興味を持っていたのはWCHAR、ニーモニック用のテキスト文字列を含むさまざまな2次元配列のイテレータを用意することです。

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;

現在のアプローチでは、テンプレートを使用して各配列のプロキシクラスを定義し、配列を表すプロキシオブジェクトを使用して特定の配列を反復処理するために使用できる単一の反復子クラスを使用します。

メモリ常駐データのコピーは、メモリ常駐データのディスクへの読み書きを処理するオブジェクトに格納されます。このクラスにCFileParaは、テンプレート化されたプロキシクラス(MnemonicIteratorDimSizeおよびその派生元のサブクラスMnemonicIteratorDimSizeBase)と反復子クラスが含まれていMnemonicIteratorます。

作成されたプロキシオブジェクトは、すべてのプロキシクラスの派生元である基本クラスによって記述されたインターフェイスを介して必要な情報にアクセスするイテレータオブジェクトにアタッチされます。その結果、複数の異なるプロキシクラスで使用できる単一のタイプのイテレータクラスができます。これは、異なるプロキシクラスがすべて同じインターフェイス、つまりプロキシベースクラスのインターフェイスを公開しているためです。

最初に、そのタイプのニーモニック用の特定のプロキシオブジェクトを生成するためにクラスファクトリに提供される識別子のセットを作成しました。これらの識別子は、ユーザーが表示したり、場合によっては変更したりすることに関心のある特定のプロビジョニングデータを識別するために、ユーザーインターフェイスの一部として使用されます。

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;

プロキシクラス

テンプレート化されたプロキシクラスとその基本クラスは次のとおりです。いくつかの異なる種類のwchar_tテキスト文字列配列に対応する必要がありました。2次元配列は、ニーモニックの種類(目的)に応じて異なる数のニーモニックを持ち、異なるタイプのニーモニックは、最大長が異なり、5つのテキスト文字と20のテキスト文字の間で異なりました。派生プロキシクラスのテンプレートは、各ニーモニックで最大文字数を必要とするテンプレートに自然に適合しました。プロキシオブジェクトが作成されSetRange()たら、メソッドを使用して、実際のニーモニック配列とその範囲を指定します。

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

};

イテレータークラス

反復子クラス自体は次のとおりです。このクラスは、現時点で必要なすべての基本的な前方反復子機能を提供します。しかし、私はそれから何かが必要になったときに、これが変更または拡張されることを期待しています。

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

プロキシオブジェクトファクトリは、ニーモニック識別子に基づいて、作成するオブジェクトを決定します。プロキシオブジェクトが作成され、返されるポインターが標準の基本クラス型であるので、異なるニーモニックセクションのどれがアクセスされているかに関係なく、インターフェイスは統一されています。このSetRange()メソッドは、プロキシが表す特定の配列要素と配列要素の範囲をプロキシオブジェクトに指定するために使用されます。

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

プロキシクラスとイテレータの使用

次のループに示すように、プロキシクラスとそのイテレータを使用してCListCtrl、ニーモニックのリストをオブジェクトに入力します。私が使用しstd::unique_ptrているのは、プロキシクラスが不要になり、std::unique_ptrスコープが外れたときに、メモリがクリーンアップされるようにするためです。

このソースコードが行うことはstruct、指定されたニーモニック識別子に対応する内に配列のプロキシオブジェクトを作成することです。次に、そのオブジェクトのイテレータを作成し、ranged forを使用してCListCtrlコントロールに入力してからクリーンアップします。これらはすべて生のwchar_tテキスト文字列であり、正確に配列要素の数になる可能性があるため、テキストがゼロで終了するように文字列を一時バッファにコピーします。

    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

そして今、範囲ベースのforループのキー反復子です。

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

使用法:

std::map<std::string,int> my_map;
// fill my_map
for (const std::string & k : keys(my_map))
{
    // do things
}

それが私が探していたものです。しかし、誰も持っていなかったようです。

おまけとして、私のOCDコードの配置を取得します。

演習として、 values(my_map)

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.