An enumeration (enum) is the preferred method to define a constrained range of options or selections within a code base. Your product’s code base likely defines dozens, if not hundreds or thousands, of unique enumerated data types. Additionally, enums are frequently used as an index into a lookup table. In fact, your product’s software likely contains code similar to this post’s example, where an enum indexes into a lookup table.
The Example
In our example (C code, C11), a public header file defines the ‘Key’ enum:
enum Key {
KEY_ARROW_UP,
KEY_ARROW_DOWN,
KEY_ENTER,
NUMBER_OF_KEYS
};
Elsewhere, in private C code, we find the enum indexing into a lookup table:
const char * GetKeyName(enum Key key) {
static const char * Names[] = {
"ArrowUp",
"ArrowDown",
"Enter"
};
assert(key < NUMBER_OF_KEYS);
return Names[key];
}
Upon review, the above code is generally acceptable. However, it is fragile in the face of future code maintenance changes. Especially given that the enum declaration is typically found in a separate source code file from the lookup table, a developer may easily miss the lookup table when modifying the enum. The sample code did employ a run-time assert, ensuring unexpected ‘key’ values do not read outside the ‘Names’ array bounds. Great! But that is a run-time assert, which falls short by:
- The assert occurs at run-time. A developer must build and execute the project to find certain faults. I want to know even earlier in the development process. Shift Left!
- The run-time assert fails to detect added or removed enum values.
- Note: gcc is great. In some situations, with optimizations activated, gcc will detect the use of an expanded enum. That being said, we can’t rely on that. Play with it here: Godbolt_with_oops_enum.
The Improved Example
Ultimately I want to use my project’s best-friend, the compiler, to help me explicitly detect changes when maintaining and building the project. After all, I might find myself modifying this code years later. As such, I especially want a build error drawing my attention to this particular lookup table when the enum expands or contracts. How? Enter the static-assert. After modifying the example, we now have:
const char * GetKeyName(enum Key key) {
static const char * Names[] = {
"ArrowUp",
"ArrowDown",
"Enter",
};
static_assert((sizeof(Names)/sizeof(Names[0]) == NUMBER_OF_KEYS),
"The above lookup table must contain an entry for each Key enum");
assert(key < NUMBER_OF_KEYS);
return Names[key];
}
And with that simple change, the code now detects and generates compile-time errors when the lookup table and the enum are no longer in agreement. Don’t believe me? Feel free to add or remove enum values via the wonderful godbolt service, check it out here.
I hope that was useful. Please comment with any additional improvements. Comment below!
Related references:
- C11 static assert: https://en.cppreference.com/w/c/language/_Static_assert
- C++11 static assert: https://en.cppreference.com/w/cpp/language/static_assert
- Past related blog: https://www.embeddedrelated.com/showarticle/1009.php
- Links to enabling static asserts in legacy compilers, and more examples of using static asserts: https://covemountainsoftware.com/2016/09/26/favorite-tricks-static-assert/
- Photo by Florian Krumm on Unsplash
This is good. I always enjoy reading your posts. What do you think about making the names array const (static const char * const Names[])?
Thank you! And sure, that suggestion would be fine, I can’t think of a negative. For most compilers/micros I suspect it doesn’t change the final build in any manner I’m aware of. The only time I’ve needed to sorry about that was on certain micros where I wanted the lookup in ROM, which might require yet another custom declaration, if memory serves.