DEV Community

Pawel Kadluczka
Pawel Kadluczka

Posted on • Updated on

Enums and Exhaustive switch statements in C++

Exhaustive switch statements are switch statements that do not have the default case because all possible values of the type in question have been covered by one of the switch cases. Exhaustive switch statements are a perfect match when working with enum types (both, scoped and unscoped). This can be illustrated by the following code:

#include <iostream>
#include <string>

enum class Color {
    Red,
    Green,
    Blue,
};

std::string getColorName(Color c) {
    switch(c) {
        case Color::Red: return "red";
        case Color::Green: return "green";
        case Color::Blue: return "blue";
    }
}

int main() {
    std::cout << "Color: " << getColorName(Color::Green) 
              << std::endl;
    return 0;
}
Enter fullscreen mode Exit fullscreen mode

Now, if a new enum member is added:

enum class Color {
    Red,
    Green,
    Blue,
    Yellow,
};
Enter fullscreen mode Exit fullscreen mode

the compiler will show warnings pointing to all the places that need to be fixed:

enum.cpp:12:12: warning: enumeration value 'Yellow' not handled in switch [-Wswitch]
    switch(c) {
           ^
enum.cpp:12:12: note: add missing switch cases
    switch(c) {
           ^
enum.cpp:17:1: warning: non-void function does not return a value in all control paths [-Wreturn-type]

Enter fullscreen mode Exit fullscreen mode

As pointed by Pierre in his comment below, to get this warning with GCC you must compile with the -Wswitch-enum switch.

If you configure warnings as errors, which you should do if you can, you will have to address all these errors to successfully compile your program.

If we used a switch statement with the default case instead of the exhaustive switch, the compiler would happily compile the program after adding the new enum member because it would be handled by the default case. Even if the default case was coded to throw an exception to help detect unhandled enum members, this exception would only be thrown at runtime and it could be too late to prevent failures. In a bigger system or application there could be many switch statements like this and without the help from the compiler it can be hard to find and test all of them. To sum up – exhaustive switch statements help quickly find usages that need to be looked at and fixed before the code can ship.

So far so good, but there is a problem – C++ allows this:

Color color = static_cast<Color>(42);
Enter fullscreen mode Exit fullscreen mode

Believe it or not, this is valid C++ as long as the value being cast is within the range of the underlying enum type. If you flow an enum value created this way through the exhaustive switch it won’t match any of the switch cases and since there is no default case the behavior is undefined.

The right thing to do is to always use enums instead of mixing integers and enums which ultimately creates the need to cast. Unfortunately, this isn’t always possible. If you receive values from users or external systems, they would typically be integer numbers that you may need to convert to an enum in your system or library and the way to do this in C++ is casting.

Because you can never trust values received from external systems, you need to convert them in a safe way. This is where the exhaustive switch statement can be extremely useful as it allows to write a conversion function like this:

Color convertToColor(int c) {
    auto color = static_cast<Color>(c);
    switch(color) {
        case Color::Red:
        case Color::Green:
        case Color::Blue:
            return color;
    }
    throw std::runtime_error("Invalid color");
}
Enter fullscreen mode Exit fullscreen mode

If a new enum member is added, the compiler will fail to compile the convertToColor function (or at least will show warnings), so you know you need to update it. For enum values outside of the defined set of members the convertToColor function throws an exception. If you use a safe conversion like this immediately after receiving the value, you will prevent unexpected values from entering your system. You will also have a single place where you can detect invalid values and reject and log incorrect invocations.


💙 If you liked this article...

I publish a weekly newsletter for software engineers who want to grow their careers. I share mistakes I’ve made and lessons I’ve learned over the past 20 years as a software engineer.

Sign up here to get articles like this delivered to your inbox.

https://www.growingdev.net/

Top comments (11)

Collapse
 
pauljlucas profile image
Paul J. Lucas • Edited

There's nothing special about scoped enums or C++ insofar as switch is concerned. Everything you said applies equally well to unscoped enums in both C++ and C.

FYI, see here and here.

Collapse
 
moozzyk profile image
Pawel Kadluczka

Fair point. Having said that, except for legacy code, there are not many scenarios where I would prefer unscoped enums over scoped enums in C++.

Collapse
 
pauljlucas profile image
Paul J. Lucas

Sure, but your whole point was about exhaustive switch statements that don't care about whether enums are scoped or not.

Thread Thread
 
moozzyk profile image
Pawel Kadluczka • Edited

Until you mentioned, I incorrectly assumed that exhaustive switch statements worked only with scoped enum types. Thank you for making me aware that it is not the case. I updated the post to remove the reference to scoped enum types.

btw. your posts about C++ are incredibly comprehensive!

Thread Thread
 
pauljlucas profile image
Paul J. Lucas

The warnings are about compiler quality-of-implementation. They have nothing to do with what either the C or C++ standards mandate.

your posts about C++ are incredibly comprehensive!

I feel that anything less does a disservice to the reader. Even if there were a case where I didn't want to discuss a particular facet of something, I'd at least mention it so the reader knows its exists and can look elsewhere for more information.

Thread Thread
 
pauljlucas profile image
Paul J. Lucas

After your change, you might consider mentioning that exhaustive switch statements apply to unscoped enumerations as well — and also apply in C.

Thread Thread
 
moozzyk profile image
Pawel Kadluczka

I changed the first paragraph to call out that exhaustive switch statement can be used for both, scoped and unscoped enum types. I think I will skip the C part.

Collapse
 
pgradot profile image
Pierre Gradot

It's worth mentioning that this is true only for Clang.

With GCC, the first code raises a warning (warning: control reaches end of non-void function [-Wreturn-type]) and the option -Wswitch-enum must be added if you want to be warned when a new value is added to the enum.

See here

Collapse
 
moozzyk profile image
Pawel Kadluczka

Thanks for pointing this out! I updated the post to include this information.

Collapse
 
pgradot profile image
Pierre Gradot

To be honest, Clang behavior seems much better to me 😅

Thread Thread
 
moozzyk profile image
Pawel Kadluczka

Agree. There is no world I wouldn't want to have this switch on.