How Removing Default Branch from Switch Statement Can Save Your Time
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.