Контейнеры STL — выбор по гарантиям, а не по названию
Выбор контейнера — одно из первых технических решений в любой структуре данных, и одно из самых частых на интервью. Ошибиться легко: std::list выглядит привлекательно для «частых вставок в середину», пока не выясняется, что на реальных данных он в три раза медленнее std::vector из-за cache miss на каждом узле. std::unordered_map кажется очевидным выбором для «быстрого поиска», пока хэш-функция не деградирует на специально подобранных ключах.
Под «STL-контейнером» в C++ понимается не просто структура данных, а связка из четырёх вещей: модель хранения (массив, дерево, хэш), итераторный интерфейс, политика инвалидации и шаблонный параметр-аллокатор. Все стандартные алгоритмы (std::sort, std::find, ranges) работают через итераторный интерфейс — им не нужно знать конкретный тип контейнера. А аллокаторы позволяют менять источник памяти, не меняя сам алгоритм.
Правильный выбор контейнера — это выбор гарантий: какую сложность, какой порядок, какую стабильность итераторов, какую модель памяти вы хотите. Полная карта — в слоях ниже.
Карта темы
- Последовательные контейнеры и адаптеры —
vector,deque,list,forward_list,array; адаптерыstack/queue/priority_queue; стратегия роста и cache locality. - Упорядоченные ассоциативные контейнеры —
map,set,multimap,multisetна красно-чёрном дереве; range queries,lower_bound, кастомные компараторы. - Неупорядоченные ассоциативные контейнеры —
unordered_map/set; хеш-таблица, load factor, рехэш, качество хэша, hash collision attack. - Итераторы и инвалидация — категории итераторов, range-based for, правила инвалидации после
insert/erase/push_back. - Аллокаторы и std::pmr —
std::allocator, кастомные аллокаторы как часть типа, полиморфные ресурсы памяти из C++17, monotonic buffer.
Сравнение сложностей
| Контейнер | Поиск | Конец (вставка) | Середина (вставка) | Random access |
|---|---|---|---|---|
vector | O(n) | O(1) аморт. | O(n) | O(1) |
deque | O(n) | O(1) | O(n) | O(1) |
list | O(n) | O(1) | O(1) после find | — |
map | O(log n) | O(log n) | O(log n) | — |
unordered_map | O(1) avg | O(1) avg | O(1) avg | — |
set | O(log n) | O(log n) | O(log n) | — |
Частые ошибки и ловушки
| Ошибка | Последствие |
|---|---|
Хранить ссылку/указатель на элемент vector через push_back | Реаллокация → UB, ссылка указывает на освобождённую память |
Использовать operator[] на std::map для проверки наличия ключа | Молча вставится элемент с дефолтным значением |
Выбирать std::list из-за O(1) вставки в середину | На малых/средних данных проигрывает vector из-за cache miss |
unordered_map без качественного хэша на внешних ключах | Hash collision attack — деградация до O(n) |
| Полагаться на симметричное XOR-комбинирование хэшей | hash(a) ^ hash(b) == hash(b) ^ hash(a) — лишние коллизии |
Сравнивать std::vector<int> и std::vector<int, MyAlloc> как один тип | Это разные шаблонные инстансы, не взаимозаменяемы |
Использовать [i] на vector без проверки границ | UB при выходе; для безопасности есть .at(i) |
pop() у адаптеров считать возвращающим значение | Не возвращает — exception-safety design; используйте top()+pop() |
iterator после рехэша unordered_map | Инвалидирован; нужно перезапросить |
Кастомный компаратор <= вместо < | Нарушение strict weak ordering → UB в RB-tree |
Значение для собеседований
Контейнеры — обязательная тема почти на каждом C++-интервью, от junior до staff. Причина: выбор контейнера обнажает понимание сложности, модели памяти и компромиссов — всего, что отличает уверенного разработчика от человека, который «знает синтаксис».
Что обычно проверяют:
- Чем
vectorотличается отlistпо фактической скорости (не только по Big-O). - Стратегия роста
vectorи что инвалидируется приpush_back. mapvsunordered_map— какие гарантии, когда что выбирать.- Что такое load factor и когда происходит рехэш.
- Категории итераторов и правила инвалидации.
- Зачем
operator[]уmapопасен, как избегать. - Что такое
std::pmrи зачем он нужен. - Как реализовать LRU-кэш на стандартных контейнерах (
list+unordered_map).
Типичный неверный ответ: «Я выбираю list, потому что вставка в середину O(1)». Это запускает обсуждение разницы между амортизированной сложностью и реальной скоростью на современном железе, где промахи кэша часто доминируют над числом сравнений.