Как реализовать итератор стиля STL и избежать распространенных ошибок?
Я создал коллекцию, для которой я хочу предоставить итератор с произвольным доступом в стиле STL. Я искал пример реализации итератора, но я не нашел его. Я знаю о необходимости перегрузки констант операторов []
и *
. Каковы требования для того, чтобы итератор был "STL-style" и каковы некоторые другие ошибки, которых следует избегать (если они есть)?
Дополнительный контекст: это для библиотеки, и я не хочу вводить какую-либо зависимость от него, если мне это действительно нужно. Я пишу свою собственную коллекцию, чтобы иметь возможность поддерживать двоичную совместимость между С++ 03 и С++ 11 с одним и тем же компилятором (поэтому STL, который, вероятно, не сломается).
Ответы
Ответ 1
На http://www.cplusplus.com/reference/std/iterator/ имеется удобная таблица, в которой подробно описываются спецификации § 24.2.2 стандарта С++ 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>
, либо поместить те же typedefs в сам итератор, либо наследовать от 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
документация iterator_facade от Boost.Iterator предоставляет то, что выглядит как хороший учебник по внедрению итераторов для связанного списка. Не могли бы вы использовать это как отправную точку для создания итератора с произвольным доступом по вашему контейнеру?
Если ничего другого, вы можете взглянуть на функции-члены и typedefs, предоставленные iterator_facade
, и использовать их как отправную точку для создания собственного.
Ответ 3
Томас Беккер написал полезную статью на эту тему здесь.
Был и этот (возможно, более простой) подход, появившийся ранее в SO: Как правильно реализовать пользовательские итераторы и константы-константы?
Ответ 4
Вот пример исходного итератора указателя.
Нельзя использовать класс итератора для работы с необработанными указателями!
#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; }
Обходной цикл на основе диапазона Raw pointer. Пожалуйста, исправьте меня, если есть лучший способ сделать цикл на основе диапазона от необработанного указателя.
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 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 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
для него и предоставить некоторый необходимый typedef
(например, iterator_category
или value_type
), либо альтернативно получить его из std::iterator
, который определяет необходимый typedef
для вас, и вы можете поэтому используется со стандартным std::iterator_traits
.
cplusplus.com
от ответственности: я знаю, что некоторые люди не любят cplusplus.com
сильно, но они предоставляют действительно полезную информацию об этом.
Ответ 6
Я был/находится в той же лодке, что и вы по разным причинам (частично образовательные, частично ограниченные). Мне пришлось переписать все контейнеры стандартной библиотеки, и контейнеры должны были соответствовать стандарту. Это означает, что если я поменяю свой контейнер на версию stl, код будет работать одинаково. Это также означало, что мне пришлось перезаписать итераторы.
В любом случае, я посмотрел на EASTL. Помимо изучения тонны о контейнерах, которые я никогда не узнавал все это время, используя контейнеры stl или через мои курсы для студентов. Основная причина в том, что EASTL более читабельна, чем сопоставление stl (я обнаружил, что это просто из-за отсутствия всех макросов и прямого стиля кодирования). Там есть какие-то нехорошие вещи (например, #ifdefs для исключений), но ничего не подавить вас.
Как уже упоминалось, посмотрите ссылку cplusplus.com на итераторы и контейнеры.
Ответ 7
Я пытался решить проблему с возможностью итерации по нескольким различным текстовым массивам, все из которых хранятся в резидентной базе данных памяти, которая представляет собой большую 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
управления 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++;
}