Enums, warnings, and default

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
switches in these scenarios:

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:

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 breaks to returns:

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 enums
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 ifdefault
is present? That is, the following could should warn when
Color::Pink is added as a new enumerator when I recompile:

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 a
case 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 a default label.

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
switches 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?).