As someone who's used at working with safety-related code, MISRA C and similar, I see some flaws in the reasoning here:
- "An enum can only have one of the values defined in the enumeration list" is false. It is just a glorified integer type.
- "That branch cannot be covered" Except it can, in case the enum gets an unexpected value.
To find unexpected execution branches like this is one of the reasons to perform static analysis/code coverage to begin with.
Now why would this enum ever get any other value than ENUM_VAL_A
, ENUM_VAL_B
, ENUM_VAL_C
? In embedded systems we might start to argue about somewhat unlikely things like electromagnetic interference, RAM memories being unreliable, cosmic rays etc. But far more likely, RAM variables will change values unexpectedly because of bugs. Pointer bugs, out of bounds access, runaway code, stack overflows and so on.
No matter what level of code quality and integrity you have, it is never wrong to include defensive programming. Meaning you add error checks to catch things that shouldn't be possible in theory. In this case by simply adding an else
:
else if (x == ENUM_VAL_C)
{
result = doThing3();
}
else
{
// this is actually not unreachable code.
}
In your specific case it most likely makes most sense to write a return -1
inside this else
. In other cases, it might be sufficient to just write else {}
. The difference between an empty else
after else if
and no else
at all is that the former means: "I have actually considered this unlikely scenario, but it will not affect the program". Whereas skipping the else
just means "I have probably not considered this scenario at all". Adding such defensive programming checks/self-documenting code cost very little in terms of execution overhead.
As for how to write this whole thing in general... as it turns out, multiple returns will actually turn the code far more readable. As would using a dedicated enum for return codes instead of "magic numbers".
I would also drop 1980s "Yoda conditions" coding style (0 == something)
and instead configure the compiler to always warn for assignment inside conditional expressions. With gcc that's -Wall
or -Wparenthesis
.
For an enum with adjacent, increasing number 0, 1, 2 etc we can also create a correspondence between an enumeration constant and a function. That is, ENUM_VAL_A
should always correspond with doOneThing
. One way to do this is to replace the long if-else chain with a function pointer jump table.
So given some enums
typedef enum
{
ENUM_VAL_A,
ENUM_VAL_B,
ENUM_VAL_C,
ENUM_N // number of enumeration constants
} my_enum_t;
typedef enum // explicit error codes over "int with magic numbers"
{
ERR_NONE,
ERR_WRONG_INPUT,
ERR_UNEXPECTED,
} err_t;
Then we can completely overhaul the function like this:
err_t myFunction (my_enum_t x)
{
if ((x != ENUM_VAL_A) &&
(x != ENUM_VAL_B) &&
(x != ENUM_VAL_C))
{
return ERR_WRONG_INPUT;
}
my_enum_t result = mutateSomething(x);
if ((result != ENUM_VAL_A) &&
(result != ENUM_VAL_B) &&
(result != ENUM_VAL_C))
{
return ERR_UNEXPECTED;
}
/* now we have truly verified that the enum isn't invalid or corrupt */
err_t (*const jumptable[ENUM_N]) (void) =
{
[ENUM_VAL_A] = doOneThing,
[ENUM_VAL_B] = doThing2,
[ENUM_VAL_C] = doThing3,
};
return jumptable[result];
}
As for code coverage, it might get a little lost when we introduce function pointers. But in terms of "cyclomatic complexity" (number of possible execution paths), then this code is much simpler since there are no (nested) if-else chains or switch
.