Hi everyone 
In this tutorial we will talk about Flat Map
So, What is Flat Map actually?
Flat Map is an associative container of C++ STL that stores data in key–value pairs, just like map. Each key inside a flat map is unique, and the elements are kept sorted by key automatically. From the outside, it looks and behaves almost exactly like map.
The difference shows up on the inside. Instead of a tree, flat map keeps its data in two parallel arrays — one holding the sorted keys and another holding the corresponding values at the same index. Because the data sits in contiguous memory, flat map is much more cache-friendly than map.
Flat map is a container adaptor (like stack, queue, and priority queue). Its underlying data structure is a pair of sequence containers — by default two vectors, but it can also be configured to use deque. The first vector stores keys in sorted order, the second stores values.
Because of this layout —
-
-
-
- Lookup is O(log n) — same as map, thanks to binary search on the sorted key array
- Iteration is faster than map — contiguous memory gives us cache locality
- Insertion and deletion are O(n) — because inserting in the middle of a vector means shifting all elements after it
-
-
So flat map is best when we build the container once and then read from it many times. If we need to insert and delete frequently, regular map is usually the better choice.
💡 Note on availability — Flat map is a C++23 feature. We need a modern compiler with C++23 support (GCC 15+, Clang 20+, or MSVC 19.40+) and the
-std=c++23flag. Before C++23, we can get very similar behavior fromboost::container::flat_map.
Header File Inclusion for flat_map
#include <flat_map>
Declaration & Initialization
Declaring an empty flat map —
flat_map <int, string> studentMap;
Here, key type is int and value type is string. The flat map is empty right now.
Declaring a flat map with values —
We can initialize a flat map with values using curly braces —
flat_map <int, string> studentMap = {
{ 103, "Sakib" },
{ 101, "Imran" },
{ 102, "Linkon" }
};
After Initialization studentMap will look like this —
| key | value |
|---|---|
| 101 | Imran |
| 102 | Linkon |
| 103 | Sakib |
Notice that even though we inserted the values in the order 103, 101, 102, flat map has sorted them in ascending order by key. That is the same behavior we see in map.
Declaring a flat map with different data types —
Key and value can be of any type, even different from each other —
flat_map <string, int> ageMap;
flat_map <char, vector<int> > charListMap;
flat_map <string, pair<int, int> > rangeMap;
Declaring a flat map in descending order of keys —
By default, flat map stores keys in ascending order. If we want keys in descending order, we can pass greater<> as the third template parameter —
flat_map <int, string, greater<int> > reverseMap;
Declaring a flat map with a different underlying container —
Since flat map is a container adaptor, we can tell it which sequence container to use for keys and values. By default both are vector, but we can switch to deque —
flat_map <int, string, less<int>, deque<int>, deque<string> > dequeMap;
Insertion
Insertion using [ ] subscript operator
The easiest way to insert a key–value pair is by using the [ ] operator —
flat_map <int, string> studentMap;
studentMap[101] = "Imran";
studentMap[102] = "Linkon";
studentMap[103] = "Sakib";
Insertion using insert() method with pair
We can also insert values using the insert() method with a pair —
studentMap.insert(pair<int, string>(104, "Rafi"));
Insertion using insert() method with make_pair
Same thing can be done with make_pair() for cleaner code —
studentMap.insert(make_pair(105, "Tanvir"));
Insertion using insert() method with curly braces
We can also insert using curly braces directly —
studentMap.insert({ 106, "Shuvo" });
Insertion using emplace() method
emplace() constructs the pair in place, so it avoids an extra copy (since we are not using pair syntax explicitly) —
studentMap.emplace(107, "Hasan");
💡 Quick tip on duplicate keys — Since flat map keys are unique, if we try to insert a key that already exists using
insert()oremplace(), the insertion will be ignored (old value stays). But if we use the[ ]operator, the old value will be overwritten. Choose carefully based on what we want.
Insertion using insert_or_assign() method
If we want the behavior of “insert if new, update if exists” explicitly, we can use insert_or_assign() —
studentMap.insert_or_assign(101, "Imran Hossain");
Now the value of key 101 will be updated to “Imran Hossain”.
Insertion using try_emplace() method
try_emplace() constructs the value in place, but only if the key does not already exist. If the key is already there, nothing happens —
studentMap.try_emplace(108, "Fahim");
studentMap.try_emplace(101, "Someone Else"); // ignored, 101 already exists
💡 Note on bulk insertion — Inserting one element at a time into a flat map is O(n) per insert (because of the shift), which means bulk loading one-by-one is O(n²). If we already have a sorted batch of values, we can use the
sorted_uniquetag to tell flat map to skip re-sorting — making bulk load O(n) total.vector< pair<int, string> > sortedData = { {201, "A"}, {202, "B"}, {203, "C"} }; studentMap.insert(sorted_unique, sortedData.begin(), sortedData.end());
Accessing the element
[ ] subscript operator
We can access the value of a given key using the [ ] operator —
cout << studentMap[102] << endl;
Output will be – Linkon
💡 Quick tip on [ ] operator — If the key does not exist, the
[ ]operator will create a new entry with that key and a default value (empty string, 0, etc.). This can silently add unwanted entries to our flat map. If we just want to read, preferat()orfind()instead.
at() method
at() method also returns the value of a given key. But unlike [ ], it does not create a new entry if the key is missing — it throws an out_of_range exception instead —
cout << studentMap.at(103) << endl;
Output will be – Sakib
find() method
find() returns an iterator to the element if the key exists, otherwise it returns end() —
auto it = studentMap.find(104);
if (it != studentMap.end())
{
cout << it->first << " " << it->second << endl;
}
else
{
cout << "Key not found" << endl;
}
Output will be – 104 Rafi
Accessing the underlying containers
Flat map lets us peek directly into the two vectors it uses internally. This is something we can only do with flat map — map and unordered_map do not expose their internals this way.
keys() method
keys() returns a read-only reference to the underlying container that holds the keys —
const auto &keyList = studentMap.keys();
for (auto key : keyList)
{
cout << key << " ";
}
values() method
values() returns a read-only reference to the underlying container that holds the values —
const auto &valueList = studentMap.values();
for (auto &value : valueList)
{
cout << value << " ";
}
💡 Quick tip on keys() and values() — These are handy when we want to hand the keys or values to an algorithm that works on contiguous memory (like passing them to a C API, or feeding them into SIMD code). We get direct access to the internal vector without any copying.
Checking if a key exists
count() method
Since keys are unique, count() returns either 0 (not present) or 1 (present) —
if (studentMap.count(105))
{
cout << "Key exists" << endl;
}
contains() method
We can also use contains() for a cleaner check —
if (studentMap.contains(105))
{
cout << "Key exists" << endl;
}
Iterating through a flat map
Range-based for loop
The cleanest way to iterate through a flat map is using a range-based for loop —
for (auto &element : studentMap)
{
cout << element.first << " - " << element.second << endl;
}
Output will be —
101 - Imran Hossain
102 - Linkon
103 - Sakib
104 - Rafi
105 - Tanvir
106 - Shuvo
107 - Hasan
108 - Fahim
Using iterator
We can also iterate using iterators directly —
flat_map <int, string>::iterator it;
for (it = studentMap.begin(); it != studentMap.end(); it++)
{
cout << it->first << " - " << it->second << endl;
}
💡 Note on iteration speed — Iterating a flat map is typically faster than iterating a map of the same size, even though both are O(n). The reason is memory layout — flat map’s keys and values sit in contiguous arrays, which means the CPU cache can pre-load the next few entries. Map stores each entry in a separate tree node scattered across the heap.
Deletion
Deletion using erase() with key
We can delete a specific key–value pair by passing the key directly to erase() —
studentMap.erase(102);
After deletion, the entry with key 102 will be removed from the flat map.
Deletion using erase() with iterator
We can also delete using an iterator —
auto it = studentMap.find(104);
if (it != studentMap.end())
{
studentMap.erase(it);
}
Deletion using erase() with range
We can delete a range of elements by passing the start and end iterators —
studentMap.erase(studentMap.find(105), studentMap.end());
clear() method
If we want to remove all elements of the flat map, we can use clear() —
studentMap.clear();
After clear(), studentMap will be empty.
💡 Note on deletion cost — Each erase() in a flat map is O(n) because the elements after the deleted one must shift left to close the gap. For bulk deletion,
erase_if()processes the whole container in a single O(n) pass instead of O(n) per element.
erase_if() method
erase_if() is a non-member helper that removes all elements matching a predicate in one pass —
// remove all entries where key is greater than 105
erase_if(studentMap, [](const auto &e) {
return e.first > 105;
});
Size and Empty
cout << studentMap.size() << endl;
if (studentMap.empty())
{
cout << "Flat map is empty" << endl;
}
size() returns the number of elements, and empty() returns true if the flat map has no elements.
Swapping
swap() method
STL flat map has a method named swap(), which swaps all the key–value pairs between two flat maps —
flat_map <int, string> mapOne = {
{ 1, "One" },
{ 2, "Two" }
};
flat_map <int, string> mapTwo = {
{ 10, "Ten" },
{ 20, "Twenty" },
{ 30, "Thirty" }
};
// swapping values between two flat maps
mapOne.swap(mapTwo);
After swapping, mapOne will have the elements of mapTwo, and mapTwo will have the elements of mapOne.
extract() and replace()
Flat map gives us two unique methods — extract() and replace() — that let us rip out its underlying vectors, work on them directly, and then hand them back.
extract() method
extract() takes the underlying key and value containers out of the flat map, leaving the flat map empty —
auto internals = std::move(studentMap).extract();
// internals.keys is a vector<int> of keys
// internals.values is a vector<string> of values
// studentMap is now empty
replace() method
replace() does the reverse — it installs two vectors as the new underlying storage of the flat map —
vector<int> newKeys = { 1, 2, 3 };
vector<string> newValues = { "A", "B", "C" };
studentMap.replace(std::move(newKeys), std::move(newValues));
The two vectors must have the same size, and the keys must already be sorted and unique. replace() does not re-sort or validate them.
💡 Quick tip on extract/replace — This pair is useful when we want to apply some heavy transformation on the keys or values (for example, bulk-update every value). We extract, modify the raw vectors directly (fast, contiguous memory), then replace — skipping the O(log n) lookup overhead that flat map would otherwise apply on every access.
lower_bound() and upper_bound()
Since flat map keys are stored in sorted order, we can use lower_bound() and upper_bound() just like we do for map.
lower_bound(key) — returns an iterator to the first element whose key is not less than the given key.
upper_bound(key) — returns an iterator to the first element whose key is strictly greater than the given key.
flat_map <int, string> studentMap = {
{ 101, "Imran" },
{ 103, "Sakib" },
{ 105, "Tanvir" },
{ 107, "Hasan" }
};
auto lb = studentMap.lower_bound(103);
auto ub = studentMap.upper_bound(103);
cout << lb->first << " " << lb->second << endl;
cout << ub->first << " " << ub->second << endl;
Output will be —
103 Sakib
105 Tanvir
Flat Map vs Map vs Unordered Map
This is the biggest question when picking between these three — so it is worth laying them side by side.
| Property | flat_map | map | unordered_map |
|---|---|---|---|
| Underlying data structure | Two sorted vectors | Red-Black Tree | Hash Table |
| Order of keys | Sorted | Sorted | No specific order |
| Lookup | O(log n) with cache locality | O(log n) | O(1) average |
| Insertion / Deletion | O(n) | O(log n) | O(1) average |
| Iteration speed | Fastest (contiguous memory) | Slow (pointer chasing) | Medium |
| Memory usage | Lowest | Higher (tree nodes) | Highest (bucket overhead) |
| C++ version | C++23 | C++98 | C++11 |
| Use when | Build once, read many — lookup-heavy workloads | Balanced insert/delete/lookup with sorted keys | Fastest lookup, order does not matter |
Time Complexity
| Operation | Description | Time Complexity |
|---|---|---|
| operator[ ] | Access or insert a value by key | O(log n) for access, O(n) for insert |
| at() | Access value by key (throws if missing) | O(log n) |
| insert() | Insert a key–value pair | O(n) |
| insert(sorted_unique, …) | Bulk insert a pre-sorted, unique range | O(n) |
| emplace() | Construct and insert a key–value pair in place | O(n) |
| insert_or_assign() | Insert new or update existing value | O(n) |
| try_emplace() | Insert only if key does not exist | O(n) |
| erase() | Remove an element by key or iterator | O(n) |
| erase_if() | Remove all elements matching a predicate | O(n) |
| find() | Find an element by key | O(log n) |
| count() | Check if a key exists (returns 0 or 1) | O(log n) |
| contains() | Check if a key exists | O(log n) |
| lower_bound() | Iterator to first key not less than given key | O(log n) |
| upper_bound() | Iterator to first key greater than given key | O(log n) |
| equal_range() | Returns a pair of iterators covering elements with the key | O(log n) |
| keys() / values() | Direct access to underlying key/value containers | O(1) |
| extract() | Move out the underlying containers | O(1) |
| replace() | Install new underlying containers | O(1) |
| size() | Returns the number of elements | O(1) |
| empty() | Returns true if flat map is empty | O(1) |
| clear() | Removes all elements | O(n) |
| swap() | Swaps contents of two flat maps | O(1) |
| begin() / end() | Iterators to the beginning and end | O(1) |
Applications
-
-
-
- Configuration tables loaded once at program start and read many times
- Lookup tables for translations, localization, or unit conversions
- In-memory indexes for read-heavy analytics workloads
- Small-to-medium associative containers where cache performance matters more than asymptotic complexity
- Embedded systems and memory-constrained environments (lower memory overhead than map)
- Replacing a manually-maintained sorted
vector<pair<K, V>>with a proper STL container - Handing the underlying arrays to a C API or SIMD code using keys() and values()
- Range queries with lower_bound() and upper_bound() where iteration speed matters
-
-
In the next tutorial we will talk about another useful STL container.
Until then
Happy Coding