контейнеры и память — устройство и практикаcontainers and memory — internals and practice

ИтераторыIterators

Иерархия категорий итераторов существует не как абстрактная классификация, а потому, что разные структуры данных физически поддерживают разные операции за O(1). А правила инвалидации — прямое следствие того, что именно происходит в памяти при вставке/удалении в конкретном контейнере.The iterator category hierarchy isn't an abstract classification — it exists because different data structures physically support different operations in O(1). And the invalidation rules are a direct consequence of what actually happens in memory on insert/erase for a given container.

фундаментfoundational частый вопросcommon interview question

Категории итераторовIterator categories

Каждая следующая категория — надмножество возможностей предыдущей. Категория итератора контейнера определяется его внутренней структурой, а не желанием автора STL.Each successive category is a superset of the previous one's capabilities. A container's iterator category is determined by its internal structure, not by the STL author's preference.

категорияcategoryоперацииoperationsпример контейнераexample container
Inputчтение вперёд, однопроходный (++it, *it)forward read, single-pass (++it, *it)std::istream_iterator
ForwardInput + многопроходность (можно сохранить и переиспользовать итератор)Input + multi-pass (can save and re-use the iterator)std::forward_list
BidirectionalForward + движение назад (--it)Forward + moving backward (--it)std::list, std::map
Random AccessBidirectional + арифметика за O(1) (it + n, it[n], it1 - it2)Bidirectional + O(1) arithmetic (it + n, it[n], it1 - it2)std::vector, std::deque

std::list — двусвязный список, поэтому переход к произвольному элементу требует прохода по ссылкам за O(n); отсюда Bidirectional, а не Random Access, несмотря на то что "назад-вперёд" список умеет. std::vector хранит элементы непрерывно, поэтому it + 5 — это просто ptr + 5 * sizeof(T), одна арифметическая операция.std::list is a doubly-linked list, so reaching an arbitrary element requires walking links in O(n); hence Bidirectional, not Random Access, despite being able to go both ways. std::vector stores elements contiguously, so it + 5 is just ptr + 5 * sizeof(T) — one arithmetic op.

Правила инвалидации по контейнерамInvalidation rules by container

Инвалидация — не произвольное правило стандарта, а прямое следствие того, что физически происходит с памятью контейнера при модификации.Invalidation isn't an arbitrary standard rule — it's a direct consequence of what physically happens to the container's memory on modification.

std::vector
std::vector<int> v{1, 2, 3};
auto it = v.begin();
v.push_back(4);    // если capacity исчерпана — реаллокация,// if capacity is exhausted — reallocation,
                     // it указывает на старый (уже освобождённый) буфер: UB// it points into the old (now freed) buffer: UB
std::list
std::list<int> l{1, 2, 3};
auto it = l.begin();
l.push_back(4);    // узлы не двигаются в памяти — it остаётся валиден// nodes never move in memory — it stays valid

std::vector: вставка инвалидирует ВСЕ итераторы/указатели/ссылки, если происходит реаллокация (превышена capacity); если реаллокации не было — инвалидирует только итераторы на элементы после точки вставки (сдвиг в памяти). Удаление всегда инвалидирует итератор на удалённый элемент и на всё, что после него (сдвиг влево).std::vector: insertion invalidates ALL iterators/pointers/references if a reallocation happens (capacity exceeded); if no reallocation occurred, it invalidates only iterators to elements after the insertion point (they shifted in memory). Erasure always invalidates the iterator to the erased element and everything after it (shifted left).

std::list: узлы — отдельные аллокации, связанные указателями; вставка/удаление никогда не двигает существующие узлы в памяти. Итератор инвалидируется, только если указывает непосредственно на удалённый узел — все остальные остаются валидны при любых вставках/удалениях в любом месте списка.std::list: nodes are separate allocations linked by pointers; insertion/erasure never moves existing nodes in memory. An iterator is only invalidated if it points directly at the erased node — everything else stays valid across any insert/erase anywhere in the list.

std::unordered_map: вставка может вызвать rehash (если load_factor превышает max_load_factor), который переставляет элементы по новым бакетам — это инвалидирует ВСЕ итераторы, но НЕ указатели/ссылки на сами элементы (узлы, а не сам элемент, переставляются между бакетами). Удаление инвалидирует только итератор на удалённый элемент.std::unordered_map: insertion may trigger a rehash (if load_factor exceeds max_load_factor), which redistributes elements across new buckets — this invalidates ALL iterators, but NOT pointers/references to the elements themselves (nodes, not the element, move between buckets). Erasure invalidates only the iterator to the erased element.

контейнерcontainerвставкаinsertionудалениеerasure
std::vectorвсё, если реаллокация; иначе — после точки вставкиeverything if reallocated; else — past the insertion pointудалённый элемент и всё после негоthe erased element and everything after it
std::dequeвсе итераторы, но не обязательно ссылки/указателиall iterators, but not necessarily references/pointersэлементы между точкой удаления и ближайшим концомelements between the erase point and the nearer end
std::list / std::forward_listничегоnothingтолько сам удалённый узелonly the erased node itself
std::map / std::setничегоnothingтолько сам удалённый узелonly the erased node itself
std::unordered_map / std::unordered_setвсе итераторы при rehash; указатели/ссылки — никогдаall iterators on rehash; pointers/references — neverтолько сам удалённый узелonly the erased node itself

Минимальная реализация: forward-итераторMinimal implementation: a forward iterator

Свой итератор для связного списка требует определить типовые алиасы (или унаследоваться от std::iterator, устаревший подход после C++17) и перегрузить operator*, operator++, operator==/operator!=.A custom iterator for a linked list requires defining the type aliases (or inheriting from std::iterator, deprecated since C++17) and overloading operator*, operator++, operator==/operator!=.

template<typename T>
class ForwardListIterator {
    struct Node { T value; Node* next = nullptr; };
    Node* node;
public:
    using iterator_category = std::forward_iterator_tag;
    using value_type        = T;
    using difference_type   = std::ptrdiff_t;
    using pointer          = T*;
    using reference        = T&;

    explicit ForwardListIterator(Node* n) : node(n) {}

    T& operator*()  const { return node->value; }
    T*  operator->() const { return &node->value; }

    ForwardListIterator& operator++() {              // префиксный// prefix
        node = node->next;
        return *this;
    }
    ForwardListIterator operator++(int) {           // постфиксный// postfix
        ForwardListIterator tmp = *this;
        node = node->next;
        return tmp;
    }

    bool operator==(const ForwardListIterator& other) const { return node == other.node; }
    bool operator!=(const ForwardListIterator& other) const { return node != other.node; }
};

Наличие обоих operator++ (префиксного и постфиксного, различаемых фиктивным параметром int) — минимальное требование для Forward-категории; Bidirectional добавил бы operator--, а Random Access — арифметику (operator+, operator-, operator[], операторы сравнения </>).Having both operator++ forms (prefix and postfix, distinguished by the dummy int parameter) is the minimum requirement for the Forward category; Bidirectional would add operator--, and Random Access would add arithmetic (operator+, operator-, operator[], relational operators </>).

ГраблиGotchas

erase() возвращает новый валидный итератор — старый уже мёртвerase() returns a new valid iterator — the old one is already dead

// неверно: it после erase невалиден, ++it — UB// wrong: it is invalid after erase, ++it is UB
for (auto it = v.begin(); it != v.end(); ++it)
    if (*it == target) v.erase(it);

// верно: присвоить результат erase() обратно// correct: assign erase()'s result back
for (auto it = v.begin(); it != v.end();)
    it = (*it == target) ? v.erase(it) : std::next(it);

reserve() заранее убирает инвалидацию из-за реаллокацииreserve() upfront removes reallocation-driven invalidation

Если известен верхний предел количества элементов, v.reserve(n) до цикла вставок гарантирует отсутствие реаллокаций внутри цикла — итераторы/указатели, полученные заранее, остаются валидны на всём протяжении, пока размер не превысит зарезервированную capacity.If an upper bound on element count is known, calling v.reserve(n) before an insertion loop guarantees no reallocations happen inside it — iterators/pointers obtained beforehand stay valid throughout, as long as size never exceeds the reserved capacity.

end() — не элемент, но тоже может инвалидироватьсяend() isn't an element, but it can still be invalidated

v.end() не разыменовывается, но сам итератор — конкретное значение (указатель за последним элементом для vector), и оно меняется при любой реаллокации. Сохранённый заранее end() для vector так же опасен после push_back, как и итератор на конкретный элемент.v.end() is never dereferenced, but the iterator itself is a concrete value (one-past-the-last-element pointer for a vector), and it changes on any reallocation. A previously saved end() for a vector is just as dangerous after a push_back as an iterator to a specific element.