EnumArray - A Practical Mapping Trick Without Paying Hash-Map Tax
Whenever I work with configuration systems, metadata tables, or low-level processing pipelines, I repeatedly run into the same pattern:
I have an enum, and I want a dictionary that maps each enumerator to some piece of data.
The naïve solution is universal and convenient:
| |
It works. It is readable. It is also wasteful.
For tiny lookup tables (and most metadata enums are tiny), a hash map is a poor fit:
- unnecessary memory overhead
- unnecessary hashing
- unnecessary indirect lookups
- no locality
- no compile-time size information
- and in the worst case, a surprising amount of hidden allocations
It is overkill for something that is basically a compile-time list of a few well-known items.
At the same time, an enum is just an integer. And an integer is a perfect array index.
This leads naturally to a simple idea: keep the dictionary interface, but back it with std::array.
What we need is only one missing piece: the size of the array.
The easiest (not the most elegant, but the most robust) method is an extra enumerator:
| |
If you’ve seen older C codebases, you’ve seen this pattern. It’s boring, but it works and requires zero metaprogramming magic.
And now the container:
| |
Usage looks identical to a map:
| |
Efficient, minimal, predictable.
Is this works well?
Performance and locality
Data is compact and cache-friendly.
No pointer chasing, no buckets, no load factors.
Compile-time size
The array size is known at compile time, which means:
- no hidden allocations
- no need for reserve()
- no dynamic resizing
- possibility of making almost everything
constexpr
Predictable semantics
Lookups are O(1) with no heuristics, no collisions, no need for custom hash functions.
If you are dealing with small fixed dictionaries (units, message types, UI states, processing steps, etc.), this is simply the most cost-effective approach.
Real-World Scenarios
Image processing filters
A pipeline might define:
| |
Mapping enum → filter parameters:
| |
Lookup is constant-time and extremely cheap. No heap usage during processing.
Logging categories
| |
Enum -> log-level:
| |
Simulation flags or constants
| |
Enum -> constants:
| |
Edge Cases and Limits
This technique is simple, but not universal. Some restrictions matter.
The enum must be contiguous and zero-based
This breaks:
| |
Your array would need 512 entries, most unused.
This is unacceptable in real systems.
Default construction is mandatory
Because the underlying array is always allocated at full size.
If T is non-default-constructible:
| |
Then EnumArray<Enum, NonDefault> fails.
Partial initialization yields default-constructed gaps
If you do this:
| |
Then:
| |
Whether that’s acceptable depends on your application.
Duplicate initializers silently overwrite
| |
Hash maps also behave this way, but with an array it’s easier to make a mistake.
Out-of-range enum values = instant UB
If someone does:
| |
You index outside of the array.
No protection unless you add explicit checks.
A Few Additional Useful Variants
Engine codebases often extend this idea:
Optional storage
Avoid default construction by storing std::optional<T>:
| |
Static compile-time initialization
| |
Everything resolved at compile time.
Bounds-checked operator()
| |
Used in high-integrity code where correctness beats speed.
When To Use EnumArray vs Map?
Use EnumArray when:
- the enum is small and fixed
- lookups are extremely frequent
- memory locality matters
- constexpr initialization is desirable
- no dynamic insertions are needed
- default-constructibility is acceptable
Use unordered_map/map when:
- enum values are non-contiguous
- you need insertion/removal at runtime
- memory overhead doesn’t matter
- the dictionary is sparse
- the enum is controlled by another subsystem and may change in unpredictable ways
Final Thoughts
EnumArray is one of those micro-optimizations that isn’t exciting but pays off in real systems. You remove the noise of hashing, reduce allocations, improve locality, and make your code more predictable. It is not universal, but for the 90% case where an enum is a closed set of states known at compile time, it delivers exactly what you need:
- dictionary-like syntax
- array-level performance
- no extra costs
I use this pattern often, and I rarely regret it.
If you are looking for a small, practical tool that helps you enforce structure and improve efficiency without introducing complexity - this is one of the simplest wins you can get in modern C++.