Intro

I can’t remember the source, but there is a general principle:

Wrong code should not compile.

In this post, I’ll show a somewhat counterintuitive approach to the switch statement.

Example

Let’s take a look at the following example:

#include <iostream>
#include "data_from_string.hpp"

void printDataType(const std::string& input) {
    const DATA_TYPE type{parseDataType(input)};

    switch (type) {
        case DATA_TYPE::INTEGER: {
            std::cout << "Integer!";
            break;
        }
        case DATA_TYPE::FLOAT: {
            std::cout << "Float!";
            break;
        }
        case DATA_TYPE::STRING: {
            std::cout << "String!";
            break;
        }
        default: {
            std::cout << "Unknown!";
        }
    }

    std::cout << std::endl;
}

For this example, we will assume that the parseDataType() function is defined in a header file provided by the third-party library data_from_string, version v1.0.0:

// data_from_string.hpp

enum class DATA_TYPE {
    INTEGER,
    FLOAT,
    STRING
};

DATA_TYPE parseDataType(const std::string& input);

Everything looks and works as expected.

Library Upgrade Catch

Now, let’s assume that the maintainer released a new version of the library, v1.1.0, where a new data type was added:

// data_from_string.hpp

enum class DATA_TYPE {
    INTEGER,
    FLOAT,
    STRING,
    DATE_TIME
};

DATA_TYPE parseDataType(const std::string& input);

So, instead of STRING as we expected before, DATE_TIME can be returned. In our code, we’ll start seeing Unknown! appear in some cases.

This issue can be caught by unit tests (if we considered more than just the initial three cases), but it could also potentially reach QA or even customers.

Making Code to Fail Early

With an assumption that we use -Wall -Werror flags, we can do the following trick: remove default: branch.

...
    switch (type) {
        case DATA_TYPE::INTEGER: {
            std::cout << "Integer!";
            break;
        }
        case DATA_TYPE::FLOAT: {
            std::cout << "Float!";
            break;
        }
        case DATA_TYPE::STRING: {
            std::cout << "String!";
            break;
        }
        // No default branch
...

So, when a new value is added to DATA_TYPE, we’ll get a compilation error like this:

main.cpp: In function ‘void printDataType(const std::string&)’:
main.cpp:15:12: error: enumeration value ‘DATE_TIME’ not handled in switch [-Werror=switch]
   15 |     switch (type) {
      |            ^

We immediately know that this upgrade will require some additional work.

Pros

A bug in the code causes a compilation error, so it most likely doesn’t even reach CI, reducing the time to bug discovery.

Cons

  • It can be overwhelming to implement if the enum class has a lot of values, and most of the values aren’t used.
  • It requires your code to be compiled with -Wall -Werror, which may be problematic for legacy projects.

Conclusion

We are always taught to write a default: branch in all our switch statements. This is good general practice, but as we saw in this example, there are cases when removing it can help save time and avoid bug handling.

Happy Coding!