библиотека · контейнерыlibrary · containers

std::flat_map / std::flat_set

Тот же интерфейс, что у std::map/std::set, та же формальная сложность O(log n), но реализация поверх отсортированного std::vector вместо красно-чёрного дерева — лучшая кэш-локальность за счёт непрерывной памяти.The same interface as std::map/std::set, the same formal O(log n) complexity, but implemented on top of a sorted std::vector instead of a red-black tree — better cache locality thanks to contiguous memory.

часто используетсяcommonly used вставка в середину — O(n), не O(log n)insertion in the middle is O(n), not O(log n) частый вопрос: почему не unordered_map, чем отличается от mapcommon question: why not unordered_map, how it differs from map

Что этоWhat it is

std::flat_map<K, V>/std::flat_set<K> — ассоциативные контейнеры с полностью совместимым интерфейсом std::map/std::set (find, lower_bound, upper_bound, operator[] для map), но физически хранящие элементы в отсортированном непрерывном массиве, а не в узлах дерева. Для map это обычно пара параллельных векторов — отдельно ключи, отдельно значения (layout "structure of arrays") — а не вектор пар {key, value}. Контраст стоит проводить не с деревом, а с хэш-таблицей из std::unordered_map (см. cpp11/unordered-containers.html): unordered_map даёт амортизированный O(1) через хэширование ценой отсутствия порядка, а flat_map сохраняет тот же формальный O(log n), что у map/set, но меняет физическую раскладку данных в памяти, а не алгоритмическую сложность.std::flat_map<K, V>/std::flat_set<K> are associative containers with a fully compatible std::map/std::set interface (find, lower_bound, upper_bound, operator[] for map), but physically storing elements in a sorted contiguous array rather than in tree nodes. For map, this is typically a pair of parallel vectors — keys separately, values separately (a "structure of arrays" layout) — rather than a vector of {key, value} pairs. The contrast worth drawing isn't with a tree but with the hash table behind std::unordered_map (see cpp11/unordered-containers.html): unordered_map gives amortized O(1) via hashing at the cost of no ordering, while flat_map keeps the same formal O(log n) as map/set but changes the physical data layout in memory, not the algorithmic complexity.

std::map — дерево, узлы разбросаны по кучеstd::map — a tree, nodes scattered across the heap
std::map<int, std::string> m = {{1, "a"}, {2, "b"}};
m.find(1);
// каждый узел — отдельная аллокация, обход — переходы по указателям// each node is a separate allocation, traversal follows pointers around
std::flat_map — отсортированный vectorstd::flat_map — a sorted vector
std::flat_map<int, std::string> m = {{1, "a"}, {2, "b"}};
m.find(1);
// тот же вызов, тот же результат — но бинарный поиск идёт по// the same call, the same result — but the binary search runs over
// непрерывному массиву ключей, кэш-дружелюбно// a contiguous array of keys, cache-friendly

Зачем ввелиWhy it was introduced

Формально одинаковая асимптотика std::map и flat_map скрывает большую разницу в реальной производительности:The formally identical asymptotics of std::map and flat_map hide a large gap in real-world performance:

01

Узлы дерева std::map разбросаны по куче — плохая кэш-локальностьstd::map's tree nodes are scattered across the heap — poor cache locality

Каждый узел красно-чёрного дерева — отдельная аллокация, физически расположенная там, куда её поместил аллокатор, без гарантии соседства с другими узлами. Поиск или обход требует перехода по указателям между несмежными участками памяти — почти каждый переход к следующему узлу означает cache miss, потому что процессор не может предсказать и заранее загрузить далёкий от текущего адрес.Each red-black-tree node is a separate allocation, physically placed wherever the allocator put it, with no guarantee of proximity to other nodes. A lookup or traversal requires following pointers between non-adjacent memory regions — nearly every hop to the next node means a cache miss, because the processor can't predict and prefetch an address that's far from the current one.

02

Непрерывный массив — последовательный доступ, префетчер работает эффективноA contiguous array means sequential access, and the prefetcher works well

flat_map хранит все ключи (а для map — и значения) в одном непрерывном блоке памяти — бинарный поиск и обход по нему обращаются к соседним или предсказуемо расположенным адресам, что позволяет аппаратному префетчеру процессора заранее подгружать нужные кэш-линии, а не ждать полного цикла обращения к оперативной памяти на каждом шаге.flat_map stores all its keys (and, for map, values too) in one contiguous memory block — binary search and traversal over it touch neighboring or predictably-located addresses, which lets the CPU's hardware prefetcher load the needed cache lines ahead of time instead of waiting on a full main-memory round trip at every step.

03

Для небольших/средних контейнеров разница ощутима на практикеFor small/medium containers, the difference is noticeable in practice

Для контейнеров из десятков-сотен элементов (типичный размер во многих реальных сценариях — конфигурация, справочники, таблицы диспетчеризации) выигрыш flat_map от кэш-локальности часто перевешивает формально одинаковую логарифмическую сложность — бенчмарки регулярно показывают flat_map быстрее std::map в несколько раз на таких размерах, несмотря на идентичную O(log n) на бумаге.For containers with tens to hundreds of elements (a typical size in many real-world scenarios — configuration, lookup tables, dispatch tables), flat_map's cache-locality win often outweighs the formally identical logarithmic complexity — benchmarks regularly show flat_map several times faster than std::map at these sizes, despite the identical O(log n) on paper.

Как это работает под капотомHow it works under the hood

По умолчанию std::flat_map<K, V> хранит данные в двух отдельных std::vector — std::vector<K> для ключей и std::vector<V> для значений, оба отсортированы синхронно по ключу (layout можно кастомизировать через шаблонный параметр container_type, но vector — значение по умолчанию). Поиск — обычный бинарный поиск по вектору ключей; найденный индекс используется для доступа к соответствующему значению во втором векторе. std::flat_set<K> проще — один отсортированный std::vector<K>.By default, std::flat_map<K, V> stores its data in two separate std::vectors — a std::vector<K> for keys and a std::vector<V> for values, both sorted in lockstep by key (the layout is customizable via a container_type template parameter, but vector is the default). Lookup is an ordinary binary search over the vector of keys; the found index is then used to access the corresponding value in the second vector. std::flat_set<K> is simpler — a single sorted std::vector<K>.

#include <flat_map>

std::flat_map<int, std::string> m = {{3, "c"}, {1, "a"}, {2, "b"}};
// конструктор сортирует пары по ключу один раз — 1, 2, 3// the constructor sorts the pairs by key once — 1, 2, 3

auto it = m.find(2);           // бинарный поиск по внутреннему массиву ключей// binary search over the internal array of keys
if (it != m.end()) std::println("{}", it->second);   // "b"// "b"

auto lb = m.lower_bound(2);   // тот же интерфейс, что у std::map// the same interface as std::map

Массовая инициализация (как в примере выше) сортирует данные один раз за O(n log n), после чего каждый отдельный поиск — O(log n) бинарным поиском. Именно поэтому flat_map особенно уместен в сценарии "построили один раз (например, из отсортированных или почти отсортированных данных), потом много читаем" — единовременная сортировка при построении амортизируется по всем последующим поискам.Bulk initialization (as in the example above) sorts the data once in O(n log n), after which every individual lookup is O(log n) via binary search. This is exactly why flat_map particularly suits the "built once (say, from sorted or nearly-sorted data), read many times" scenario — the one-time sort at construction is amortized across all subsequent lookups.

Примеры из реального кодаReal-world examples

типичные сценарии использованияtypical usage scenarios
01

Таблица диспетчеризации, построенная один раз при стартеA dispatch table built once at startup

Отображение имени команды на обработчик — заполняется один раз при инициализации, дальше только читается на каждый входящий запрос, часто в горячем пути.A mapping from a command name to its handler — filled once at initialization, then only read on every incoming request, often on a hot path.

std::flat_map<std::string, std::function<void()>> handlers = {
    {"start", on_start},
    {"stop",  on_stop},
    {"status", on_status},
};

if (auto it = handlers.find(cmd); it != handlers.end()) it->second();
02

Небольшая конфигурация, читаемая много раз за время работыA small configuration read many times during runtime

Настройки приложения — десятки ключей, загружаются один раз при старте, дальше только читаются — идеальный случай для flat_map вместо map или unordered_map.Application settings — dozens of keys, loaded once at startup, only read thereafter — an ideal case for flat_map over map or unordered_map.

std::flat_map<std::string, std::string> config = load_config_file("app.conf");

std::string_view timeout = config.at("timeout_ms");
std::string_view host    = config.at("host");
03

Множество допустимых значений через flat_setA set of allowed values via flat_set

Проверка, входит ли значение в фиксированный набор допустимых (например, разрешённые коды валют или статусы) — набор известен заранее и не меняется во время работы.Checking whether a value belongs to a fixed set of allowed ones (say, permitted currency codes or statuses) — the set is known in advance and doesn't change at runtime.

std::flat_set<std::string> allowed_currencies = {"USD", "EUR", "GBP", "JPY"};

bool is_supported(std::string_view code) {
    return allowed_currencies.contains(code);
}
04

Диапазонный запрос через lower_bound/upper_boundA range query via lower_bound/upper_bound

Получить все записи с ключом в заданном диапазоне — тот же интерфейс, что у std::map, но обход диапазона в flat_map — это последовательный проход по непрерывной памяти, а не переходы по дереву.Getting all entries with a key in a given range — the same interface as std::map, but iterating the range in flat_map means a sequential walk over contiguous memory, not hops through a tree.

std::flat_map<int, std::string> events = {{100, "a"}, {200, "b"}, {300, "c"}};

auto from = events.lower_bound(150);
auto to   = events.upper_bound(250);
for (auto it = from; it != to; ++it) std::println("{}", it->second);   // "b"// "b"

ГраблиGotchas

Вставка в середину — O(n), а не O(log n)Insertion in the middle is O(n), not O(log n)

Вставка нового элемента в flat_map/flat_set требует сдвинуть все элементы после точки вставки в отсортированном массиве, чтобы освободить место — O(n) в худшем случае, против O(log n) у std::map, где вставка в дерево — это только локальная перебалансировка нескольких узлов без сдвига большого блока памяти. flat_map категорически не подходит для сценариев с частыми произвольными вставками/удалениями в большой контейнер — только для "построили один раз, потом много читаем" или "редко модифицируем" (см. примеры выше).Inserting a new element into flat_map/flat_set requires shifting every element after the insertion point in the sorted array to make room — O(n) in the worst case, versus O(log n) for std::map, where inserting into a tree only means local rebalancing of a few nodes with no shifting of a large memory block. flat_map is categorically unsuited to scenarios with frequent arbitrary insertions/deletions into a large container — only for "built once, read many times" or "rarely modified" (see the examples above).

Итераторы/указатели/ссылки инвалидируются при ЛЮБОЙ модификацииIterators/pointers/references are invalidated by ANY modification

В std::map ссылки на существующие элементы остаются валидными при вставке других элементов — стабильность ссылок дерева, где вставка не перемещает уже существующие узлы. В flat_map/flat_set вставка или удаление могут сдвинуть весь блок памяти после точки изменения (а при реаллокации вектора — переместить вообще всё) — итераторы, указатели и ссылки на элементы инвалидируются при любой модификации контейнера, без исключений. Код, полагающийся на стабильность ссылок std::map (например, хранящий указатель на значение в другой структуре данных), нельзя просто заменить на flat_map без пересмотра этой логики.In std::map, references to existing elements stay valid across insertion of other elements — the tree's reference stability, since insertion doesn't move already-existing nodes. In flat_map/flat_set, insertion or deletion can shift the entire block of memory after the point of change (and, on vector reallocation, move everything) — iterators, pointers, and references to elements are invalidated on any container modification, with no exceptions. Code relying on std::map's reference stability (say, storing a pointer to a value inside another data structure) can't simply be swapped to flat_map without revisiting that logic.

Не путать с unordered_map — разные компромиссы, разные сценарииDon't confuse it with unordered_map — different trade-offs, different scenarios

flat_map — не более быстрая замена unordered_map, это другая точка на карте компромиссов: сохраняет упорядоченность и O(log n) поиска, как у map, но с лучшей кэш-локальностью за счёт непрерывной памяти; unordered_map даёт амортизированный O(1) через хэширование ценой отсутствия порядка обхода. Выбор между ними определяется тем, нужен ли упорядоченный обход/диапазонные запросы (тогда flat_map или map) или чистая скорость точечного поиска без порядка (тогда unordered_map) — см. cpp11/unordered-containers.html для контраста реализации через separate chaining.flat_map isn't simply a faster drop-in for unordered_map — it's a different point on the trade-off map: it keeps ordering and O(log n) lookup like map, but with better cache locality thanks to contiguous memory; unordered_map gives amortized O(1) via hashing at the cost of no iteration order. The choice between them comes down to whether ordered iteration/range queries are needed (then flat_map or map) or raw point-lookup speed with no ordering (then unordered_map) — see cpp11/unordered-containers.html for the contrasting separate-chaining implementation.