This post describes a particular software lifetime issue I run into a lot, and a solution I use for it that I’m not particularly fond of. I’m writing this post in the hope that other people have run into the same issues and either have better ideas about how to solve it now, or better ideas of how it could be solved in the future.
Let’s say we’re using a library that defines some enum
:
namespace lib {
enum class Color {
Red,
Green,
Blue
};
}
And we have some point in our code which receives this enum
and has to react to it in some way. Maybe each enumerator needs to be treated differently, maybe not, but each enumerator does need to be carefully considered. One of the warnings I am a huge fan of is when compilers warn about non-exhaustive switch
es in these scenarios:
void foo(lib::Color c) {
switch (c) {
case lib::Color::Red: std::cout << "Red"; break;
case lib::Color::Green: std::cout << "Green"; break;
}
}
gcc, for instance, will emit:
warning: enumeration value 'Blue' not handled in switch [-Wswitch]
12 | switch (c) {
| ^
clang emits the same, with slightly different formatting. This is great! This warning means that if lib
ever decides to extend its functionality by adding something like Pink
, when I recompile my application, I will get this same warning indicating to me that I forgot about Color::Pink
and I need to handle that case.
Perfect, right? What’s not to love.
Runtime always causes problems
Except what happens if rather than recompiling against the new version of this library, I am instead linking against the new version. Now, it’s possible, that I might get a new value of lib::Color
that I wasn’t previously expecting.
How do I guard against that case?
One non-solution is to use the standard language feature for “and everything else,” namely default
:
void foo(lib::Color c) {
switch (c) {
case lib::Color::Red: std::cout << "Red"; break;
case lib::Color::Green: std::cout << "Green"; break;
case lib::Color::Blue: std::cout << "Blue"; break;
default: std::cout << "Unknown color: " << static_cast<int>(c);
}
}
This perfectly handles any new Color
enumerations that we didn’t know about when we originally compiled. But default
completely kills the warning, since now we definitely handle all cases! Now, I won’t know that I need to handle Color::Pink
when I recompile, so unless I’m extremely diligent, it will end up getting logged as an unknown color.
Which is to say, it will end up getting logged as an unknown color.
The solution is always a lambda
My solution to this problem is to wrap this in an immediately-invoked lambda expression, changing all the break
s to return
s:
void foo(lib::Color c) {
[&]{
switch (c) {
case lib::Color::Red: std::cout << "Red"; return;
case lib::Color::Green: std::cout << "Green"; return;
case lib::Color::Blue: std::cout << "Blue"; return;
}
std::cout << "Unknown color: " << static_cast<int>(c);
}();
}
This ensures that I still get a warning when Color::Pink
is added while also ensuring that I can handle unexpected new values at runtime. It seems like the best of both worlds right?
But it’s such an awkward construction. It’s like we have this specific language feature to handle this specific case (i.e. default
), and I’m explicitly eschewing it to roll my own. That seems fundamentally wrong to me. Does anyone know of a better way to do it?
Note that in this specific case, the lambda is unnecessary since there is nothing else going on in this function – so imagine that there is more code below the switch
that needs to be run in all cases.
Paper Trail
A few years ago, there was a paper to add an attribute to enum
s to mark them as exhaustive (P0375). The proposal itself was rejected, and that seems to have been the correct decision since compilers seem to always warn on the cases that paper wanted to get warnings on anyway. But maybe that’s the idea I’m actually looking for: an attribute to mark on the switch
statement to warn if any enumerator is missing even if default
is present? That is, the following could should warn when Color::Pink
is added as a new enumerator when I recompile:
void foo(lib::Color c) {
[[exhaustive]] switch (c) {
case lib::Color::Red: std::cout << "Red"; break;
case lib::Color::Green: std::cout << "Green"; break;
case lib::Color::Blue: std::cout << "Blue"; break;
default: std::cout << "Unknown color: " << static_cast<int>(c);
}
}
This seems like the sort of thing that’s worthwhile to try to implement in clang and see if people actually like it. Maybe I’ll try to figure out how to do that.
The warning that exists!
On Twitter, Rob Pilling pointed out to me that both gcc and clang already have exactly the warning I was looking for: -Wswitch-enum
. Thank you, Rob! From gcc’s docs:
Warn whenever a
switch
statement has an index of enumerated type and lacks acase
for one or more of the named codes of that enumeration.case
labels outside the enumeration range also provoke warnings when this option is used. The only difference between-Wswitch
and this option is that this option gives a warning about an omitted enumeration code even if there is adefault
label.
enum class Color {
Red,
Green,
Blue
};
int incomplete_no_default(Color c) {
switch (c) { // warns under -Wswitch
case Color::Red: return 1;
case Color::Green: return 2;
}
return 7;
}
int incomplete_with_default(Color c) {
switch (c) { // warns under -Wswitch-enum
case Color::Red: return 1;
case Color::Green: return 2;
default: return 7;
}
}
That’s perfect. I have yet to try it out and see how it works on a larger code base. Since this is a global setting, and I’m sure I have at least a few switch
es that are not intended to be exhaustive – that is, where default
is intended to cover many cases. I’m curious to see where the breakdown ends up being (that is, how many #pragma
’s will I need to write?).