C++23 Flat Containers: std::flat_map & std::flat_set Deep Dive
Master C++23 flat containers: std::flat_map and std::flat_set.
20+ years shipping performance-critical C and C++ systems. Drawn from code that ran under real load.
- ✓Basic knowledge of C++ templates and STL containers (std::vector, std::set).
- ✓Familiarity with C++20/23 features (concepts, comparisons).
- ✓Understanding of time complexity and cache effects.
- Flat containers store elements in contiguous memory (like vectors) but maintain sorted order like std::set/map.
- They offer faster iteration and lower memory overhead than node-based trees, at the cost of slower insertions/erasures.
- std::flat_map and std::flat_set are part of C++23, providing a sorted associative container backed by a sorted vector.
- Use them when lookups and iteration dominate, and modifications are infrequent.
- They support heterogeneous lookup and transparent comparators for efficient key searches.
Imagine a library where books are stored on shelves in alphabetical order. A traditional std::set is like having each book in a separate box (node) linked together – finding a book is fast, but walking through all books means jumping between boxes. A flat container is like placing all books in a single long shelf – you can walk through them quickly, but inserting a new book might require shifting many others to keep the order. Choose flat containers when you mostly read and rarely add new books.
In modern C++ development, performance often hinges on data structure choice. The C++23 standard introduces flat containers – std::flat_map, std::flat_set, std::flat_multimap, and std::flat_multiset – which combine the sorted associative interface of std::map/set with the contiguous memory layout of std::vector. This design offers superior cache locality, faster iteration, and reduced memory overhead, making them ideal for read-heavy workloads.
Consider a real-time analytics system processing millions of events per second. Using std::map for lookups by key is convenient, but the node-based tree causes cache misses and memory fragmentation. Switching to a flat container can halve lookup times and improve throughput. However, insertions and deletions become O(n) due to shifting elements, so flat containers shine when modifications are infrequent.
This tutorial covers everything you need to know: from basic usage to advanced customization, performance benchmarks, and debugging production issues. You'll learn how to choose between flat and node-based containers, how to leverage heterogeneous lookup, and how to avoid common pitfalls. By the end, you'll be equipped to integrate flat containers into your high-performance C++ projects.
What Are Flat Containers?
Flat containers are associative containers that store elements in a sorted contiguous array (like std::vector) rather than a node-based tree. C++23 introduces std::flat_map, std::flat_set, std::flat_multimap, and std::flat_multiset. They provide the same interface as std::map/set but with different performance characteristics.
Internally, a flat container holds a sorted vector of key-value pairs (for map) or just keys (for set). Lookups use binary search (O(log n)), iteration is O(n) with excellent cache locality, and insertion/erasure are O(n) due to shifting elements.
Example: Basic usage of std::flat_map.
Performance Characteristics and Trade-offs
Understanding when to use flat containers requires comparing them to node-based alternatives.
- Lookup: Both use O(log n) binary search. However, flat containers benefit from cache locality: the entire data structure fits in fewer cache lines, reducing miss penalties.
- Iteration: Flat containers are O(n) with sequential memory access, often 2-5x faster than tree iteration.
- Insertion: Node-based containers are O(log n) amortized; flat containers are O(n) due to shifting. For bulk insertions, you can sort and merge.
- Memory: Flat containers store elements contiguously with minimal overhead (just the vector capacity). Node-based containers have per-element overhead (pointers, color bits).
Benchmark: Inserting 100k elements then iterating 10 times.
Customizing the Underlying Container
Flat containers are templates that accept a sequence container as the underlying storage. By default, they use std::vector, but you can use std::deque or a custom container that satisfies the requirements (contiguous storage is not required, but recommended for performance).
You can also specify a custom comparator and a container adaptor for key extraction (for maps).
Example: Using std::deque as the underlying container for std::flat_set.
Heterogeneous Lookup and Transparent Comparators
Flat containers support heterogeneous lookup when the comparator is transparent (has is_transparent type). This allows searching for a key using a different type without constructing a temporary key object, saving allocations and copies.
Example: Searching a flat_set of strings with a string_view.
Insertion and Erasure Strategies
Because flat containers shift elements on insertion/erasure, bulk operations should be optimized.
- Range insertion: Use the range constructor or insert(first, last) to sort all elements at once.
- insert_or_assign: For flat_map, use insert_or_assign to update existing keys without extra lookup.
- Erasure: Erasing a range is O(n) but can be efficient if you erase from the end.
Example: Efficient bulk insertion using a sorted vector.
Memory Management and Allocators
Flat containers use the underlying container's allocator. You can provide a custom allocator for memory pooling or special allocation strategies.
Example: Using a custom allocator with std::flat_set.
Thread Safety and Concurrent Access
Flat containers are not thread-safe for concurrent writes. Multiple readers are safe if no writer is active. For concurrent access, use external synchronization (mutex) or consider concurrent containers.
Example: Protecting a flat_map with a mutex.
The Cache-Miss Catastrophe: How Flat Containers Saved a Trading Platform
- Profile before optimizing: CPU cache misses are a common hidden bottleneck.
- Flat containers are excellent for read-heavy, write-rare workloads.
- Always measure the actual cost of insertions vs lookups in your domain.
- Consider memory layout as a first-class performance factor.
- Use heterogeneous lookup to avoid temporary key allocations.
perf stat -e cache-misses ./appgdb -ex 'run' -ex 'bt'| File | Command / Code | Purpose |
|---|---|---|
| flat_map_basic.cpp | int main() { | What Are Flat Containers? |
| benchmark.cpp | int main() { | Performance Characteristics and Trade-offs |
| custom_container.cpp | int main() { | Customizing the Underlying Container |
| heterogeneous.cpp | struct StringLess { | Heterogeneous Lookup and Transparent Comparators |
| bulk_insert.cpp | int main() { | Insertion and Erasure Strategies |
| custom_allocator.cpp | template | Memory Management and Allocators |
| thread_safe.cpp | std::flat_map | Thread Safety and Concurrent Access |
Key takeaways
Common mistakes to avoid
3 patternsUsing flat containers for write-heavy workloads without profiling.
Forgetting to reserve capacity before bulk insertions.
Assuming iterators remain valid after insertion or erasure.
Interview Questions on This Topic
Explain the trade-offs between std::set and std::flat_set.
Frequently Asked Questions
20+ years shipping performance-critical C and C++ systems. Drawn from code that ran under real load.
That's C++ Advanced. Mark it forged?
3 min read · try the examples if you haven't