Контейнеры STL в C++: чек-лист перед выбором
vector, list, map, unordered_map — четыре контейнера, между которыми чаще всего мечется код. Чек-лист по итерациям, инвалидации и реальной стоимости операций, чтобы выбор не превратился в 200 мс на ровном месте.

Классическая ошибка: взяли std::list, потому что «часто вставляем в середину», получили в три раза более медленный проход по данным, чем на std::vector, — и не поняли почему. Или выбрали std::map, где нужен был std::unordered_map, и логарифм на каждом поиске превратился в заметную долю времени функции. Контейнер STL выбирается не по названию операции, а по тому, что с ним будет происходить в горячем цикле. Ниже — чек-лист, который проходишь до того, как зафиксировать тип. Ориентир — C++17 и новее (структурные привязки, try_emplace, contains для C++20).
Это разбор про выбор, а не про устройство красно-чёрного дерева. Внутренности упоминаются ровно настолько, насколько влияют на решение.
Вопрос первый: как вы обращаетесь к данным
Прежде чем спорить про сложность вставки, ответьте, что происходит чаще: обход всех элементов подряд или поиск по ключу. От этого зависит половина выбора.
- Обход подряд, доступ по индексу —
std::vector. Данные лежат в непрерывной памяти, префетчер процессора работает на вас, кеш не промахивается. Ни один узловой контейнер (list,map,set) к этому близко не подойдёт. - Поиск по ключу —
std::unordered_map(в среднем O(1)) илиstd::map(O(log n), но с упорядоченным обходом). - Проверка «есть ли элемент» в наборе —
std::unordered_set/std::set.
Непрерывная память бьёт алгоритмическую сложность на реальных объёмах. Проход поvectorиз миллиона int почти всегда быстрее прохода поlistиз тех же элементов — даже если у списка вставка «дешевле» на бумаге.
Вопрос второй: инвалидация итераторов и ссылок
Это то, что ломает код молча — компилятор ничего не скажет, а при следующей вставке вы получите обращение к освобождённой памяти. Держите таблицу перед глазами.
| Контейнер | Что делает вставка с итераторами | Что со ссылками на элементы |
|---|---|---|
vector | Реаллокация инвалидирует все; без реаллокации — с точки вставки и дальше | Инвалидируются при реаллокации |
deque | Вставка в конец/начало инвалидирует итераторы, но не ссылки | Остаются валидными при вставке по краям |
list | Не инвалидирует ничего | Остаются валидными всегда |
map / set | Не инвалидирует ничего | Остаются валидными всегда |
unordered_map / unordered_set | Рехеш инвалидирует итераторы, но не ссылки на элементы | Остаются валидными (элементы в узлах) |
Практический вывод: если вы держите указатель или ссылку на элемент и параллельно вставляете новые, vector — мина. Для такого сценария берут deque, list или узловой ассоциативный контейнер.
Вопрос третий: реальная стоимость операций
Асимптотика — это половина правды. Вторая половина — константа и поведение кеша.
vector: вставка не в конец дорогая
Вставка в конец через push_back — амортизированное O(1). Вставка или удаление в середине — O(n), потому что сдвигаются все элементы правее. Если вы часто удаляете из середины и порядок не важен, используйте приём swap-and-pop:
#include <vector>
#include <utility>
// Удаление элемента по индексу без сдвига хвоста.
// Порядок элементов при этом нарушается — годится, когда он не важен.
void remove_unordered(std::vector<int>& data, std::size_t index) {
std::swap(data[index], data.back()); // меняем с последним
data.pop_back(); // удаляем последний за O(1)
}map против unordered_map
std::map — дерево, ключи всегда отсортированы, поиск O(log n), обход в порядке ключей. std::unordered_map — хеш-таблица, поиск в среднем O(1), но обход в произвольном порядке и риск деградации до O(n) при плохой хеш-функции или атаке на коллизии.
Берите map, когда нужен упорядоченный обход, диапазонные запросы (lower_bound, upper_bound) или предсказуемая латентность без всплесков от рехеша. Берите unordered_map, когда нужен максимально быстрый поиск и порядок безразличен.
Вопрос четвёртый: вставка без лишних копий
Частая потеря производительности — временный объект, который создаётся и тут же копируется в контейнер. С C++11 есть emplace, с C++17 — try_emplace для ассоциативных контейнеров.
#include <map>
#include <string>
std::map<int, std::string> cache;
// try_emplace конструирует значение только если ключа ещё нет.
// Если ключ есть — второй аргумент даже не вычисляется.
cache.try_emplace(42, "expensive_default");
// insert_or_assign обновляет значение, если ключ уже есть.
cache.insert_or_assign(42, "new_value");Разница с operator[]: map[key] = value сначала вставит дефолтный элемент, потом присвоит — для тяжёлых типов это лишняя работа. try_emplace её избегает.
Чек-лист перед фиксацией типа
- Что чаще — обход или поиск по ключу? Обход подряд —
vector. Поиск — ассоциативный контейнер. - Нужен ли порядок элементов? Да, по ключу —
map/set. Порядок вставки —vector. Безразличен —unordered_*. - Держите ли вы ссылки/указатели на элементы при модификации? Да — избегайте
vector, смотрите таблицу инвалидации. - Знаете ли размер заранее? Да и это
vector— вызовитеreserve(), чтобы убить реаллокации. - Часто вставляете/удаляете в середину? Да и порядок важен —
list. Порядок не важен — swap-and-pop наvector. - Ключи — строки или сложные объекты? Для
unordered_*проверьте, что хеш-функция вменяемая; иначеmapпредсказуемее.
Что ломается на практике
- Итератор после
erase.vector::eraseи ассоциативные контейнеры возвращают итератор на следующий элемент — используйте его:it = c.erase(it);. Не инкрементируйте вручную удалённый итератор. operator[]у map создаёт элемент. Обращениеm[key]к отсутствующему ключу молча вставит дефолтное значение. Для проверки наличия —find()илиcontains()(C++20).- Реаллокация vector в цикле. Push_back в цикле без
reserve— это серия реаллокаций и копирований. Если храните указатели на элементы этого же вектора — они станут висячими. - Хеш пользовательского типа. Чтобы положить свой тип в
unordered_map, нужна специализацияstd::hashи оператор==. Забыли — не скомпилируется, но сообщение об ошибке будет длинным и неочевидным.
Инвалидация в многопоточном доступе — отдельная тема: контейнеры STL не потокобезопасны для одновременной записи, здесь это не рассматриваем.
Что делать дальше
После выбора контейнера прогоните горячий участок под профилировщиком — perf на Linux или встроенный профайлер вашей IDE — и посмотрите на промахи кеша (cache-misses в perf), а не только на время. Если vector с reserve и swap-and-pop закрывает сценарий, узловые контейнеры чаще всего проигрывают. Для узкоспециализированных случаев смотрите в сторону std::flat_map (C++23) — упорядоченный map поверх непрерывной памяти, который в чтении обгоняет обычный map.
Тендерный специалист 44-ФЗ / 223-ФЗ: Разбор PDF-документации закупки, поиск «заточек» под поставщика, чек-лист заявки.
Спросить за 20 ₽Источники: cppreference: Containers library, cppreference: Iterator invalidation
Частые вопросы
У меня C++11, а в статье try_emplace и contains — что делать?
Всегда ли vector быстрее list?
Почему unordered_map иногда медленнее map, хотя поиск O(1)?
Что вернёт map[key], если ключа нет?
Как понять, что я выбрал не тот контейнер, если код уже работает?
Можно ли положить свой класс в unordered_map ключом?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.