Як реалізувати ітератор стилю STL та уникнути загальних підводних каменів?


306

Я створив колекцію, для якої хочу надати ітератор у випадковому доступі у стилі STL. Я шукав приклад реалізації ітератора, але не знайшов. Я знаю про необхідність const перевантажень []та *операторів. Які вимоги до ітератора мають бути в стилі "STL" та які ще інші підводні камені слід уникати (якщо такі є)?

Додатковий контекст: це для бібліотеки, і я не хочу вводити будь-яку залежність від неї, якщо мені справді не потрібно. Я пишу власну колекцію, щоб мати змогу забезпечити бінарну сумісність між C ++ 03 та C ++ 11 з тим самим компілятором (так що немає STL, який, ймовірно, може зламатися).


13
+1! Приємне запитання. Я дивувався тому ж. На основі Boost.Iterator досить просто провести щось разом, але напрочуд важко просто знайти список вимог, якщо ви реалізуєте це з нуля.
jalf

2
Пам'ятайте також, що ваші ітератори повинні бути СКАРІ. boost.org/doc/libs/1_55_0/doc/html/intrusive/…
alfC

Відповіді:


232

http://www.cplusplus.com/reference/std/iterator/ має зручну діаграму, яка детально описує характеристики пункту 24.2.2 стандарту C ++ 11. В основному, ітератори мають теги, які описують дійсні операції, а теги мають ієрархію. Нижче суто символічні, ці класи насправді не існують як такі.

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>, або помістити ті самі типиdedefs в самому ітераторі, або успадкувати від std::iterator(у яких є ці typedefs). Я віддаю перевагу другому варіанту, щоб уникнути зміни речей у stdпросторі імен та читабельності, але більшість людей успадковує саме з них 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_tag, std::output_iterator_tag, std::forward_iterator_tag, std::bidirectional_iterator_tag, або std::random_access_iterator_tag, в залежності від того, які вимоги вашого итератор задовольняє. В залежності від вашого ітератора, ви можете вибрати спеціалізацію std::next, std::prev, std::advanceі std::distanceяк добре, але це рідко. У вкрай рідкісних випадках ви можете спеціалізуватися std::beginі std::end.

Напевно, у вашому контейнері також має бути const_iteratorітератор, що є (можливо, мінливим) постійним даними, подібним до вашого, iteratorза винятком того, що він має бути неявно сконструйований з а, iteratorі користувачі не повинні змінювати дані. Загальний внутрішній покажчик є вказівником на непостійні дані та iteratorуспадковує const_iteratorтак, щоб мінімізувати дублювання коду.

У моєму дописі написання власного контейнера STL є більш повний прототип контейнера / ітератора.


2
Окрім того, щоб самостійно спеціалізуватись std::iterator_traitsабо визначити typedefs, ви також можете вивести з них std::iterator, що визначає ті, які вам потрібні , залежно від параметрів шаблону.
Крістіан Рау

3
@LokiAstari: Повна документація досить обширна (40 чорних сторінок у чернеті), і не входить до сфери переповнення стека. Однак я додав більше інформації, де деталізується теги ітератора та const_iterator. Чого ще не вистачало моєму посту? Здається, ви маєте на увазі, що до класу слід додати більше, але питання стосується конкретного впровадження ітераторів.
Mooing Duck

5
std::iteratorбуло запропоновано застаріти в C ++ 17 ; це не було, але я б не заперечував, щоб він був довше довше.
einpoklum

2
Оновлення коментаря @ einpoklum: std::iteratorврешті-решт застаріле.
перегляньте

1
@JonathanLee: Нічого собі, operator boolце неймовірно небезпечно. Хтось спробує використати це для виявлення кінця діапазону while(it++), але все це дійсно перевіряється, чи був ітератор побудований з параметром.
Mooing Duck

16

Документація iterator_facade від Boost.Iterator надає те, що виглядає як приємний підручник щодо впровадження ітераторів для пов'язаного списку. Чи можете ви використовувати це як вихідну точку для побудови ітератора випадкового доступу над контейнером?

Якщо нічого іншого, ви можете ознайомитись з функціями та typedefs, що надаються членами, 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на ньому, і надати деякі необхідні typedefs (наприклад, iterator_categoryабо value_type), або альтернативно отримати їх std::iterator, що визначає необхідні typedefдля вас s, і тому можна використовувати за замовчуванням std::iterator_traits.

відмова від відповідальності: Я знаю, що деяким людям це не дуже подобається cplusplus.com, але вони надають справді корисну інформацію про це.


Я дійсно не отримую суперечки між cplusplus і cppreference, вони є хорошими і відсутні багато речей. Однак C ++ є єдиною мовою, де реалізація стандартних ітераторів бібліотеки - це пекло XD. У більшості випадків простіше писати клас обгортки над контейнером stl, ніж впроваджувати ітератор XD
CoffeDeveloper

@GameDeveloper перевірити цю бібліотеку шаблонів, яку я написав для впровадження ітераторів: github.com/VinGarcia/Simple-Iterator-Template . Це дуже просто і вимагає лише близько 10 рядків коду для написання ітератора.
VinGarcia

Гарний клас, я оцінюю це, напевно, варто перенести його для компіляції також з контейнерами, які не є STL (EA_STL, UE4). Врахуйте! :)
CoffeDeveloper

У будь-якому випадку, якщо єдиною причиною є те, що cplusplus.com надає дійсно корисну інформацію, cppreference.com надає більше корисної інформації ...
LF

@LF Тоді не соромтеся повернутися у минуле та додати цю інформацію до версії сайту 2011 року. ;-)
Крістіан Рау

3

Я був / перебуваю в тому ж човні, що і ви з різних причин (частково освітніх, частково обмежених). Мені довелося переписати всі контейнери стандартної бібліотеки, і контейнери повинні були відповідати стандарту. Це означає, що якщо я поміняю свій контейнер версією stl , код буде працювати однаково. Що також означало, що мені доведеться переписати ітератори.

У всякому разі, я подивився на EASTL . Крім того, щоб дізнатися тонну про контейнери, про яку я ніколи не вчився, використовуючи контейнери stl або через мої бакалаврські курси. Основна причина полягає в тому, що EASTL читабельніше, ніж аналог stl (я виявив, що це просто через відсутність усіх макросів і прямого стилю кодування вперед). Там є деякі примхливі речі (наприклад, #ifdefs за винятками), але нічого не може вас переповнити.

Як згадували інші, подивіться посилання cplusplus.com на ітератори та контейнери.


3

Я намагався вирішити проблему з можливістю повторення декількох різних текстових масивів, всі вони зберігаються у великій базі даних резидентів пам'яті struct.

Далі було розроблено за допомогою програми Visual Studio 2017 Community Edition на тестовій програмі MFC. Я включаю це як приклад, оскільки ця публікація була однією з декількох, на які я зіткнувся, що надав допомогу, але все ще недостатньо для моїх потреб.

Дані, structщо містять резидентні дані пам'яті, виглядали приблизно так. Я видалив більшу частину елементів заради стислості, а також не включив визначені попередніми процесорами (використовуваний SDK призначений для C, а також C ++ і є старим).

Мені було цікаво робити ітератори для різних WCHARдвовимірних масивів, які містили текстові рядки для мнемоніки.

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масивів текстових рядків. Двомірні масиви мали різну кількість мнемоніки, залежно від типу (призначення) мнемоніки, а різні типи мнемоніки були різної максимальної довжини, коливаючись між п'ятьма текстовими символами та двадцятьма текстовими символами. Шаблони для похідного проксі-класу природно підходили до шаблону, вимагаючи максимальної кількості символів у кожній мнемонічній. Після створення об’єкта проксі ми використовуємо 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якого відповідає вказаному мнемонічному ідентифікатору. Потім він створює ітератор для цього об'єкта, використовує діапазон, 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

А тепер ітератор ключів для циклу на основі діапазону.

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)

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.