Before we start, let’s get something straight; for over 30 years now I have had a love-hate relationship with the C programming language. The ‘engineer’ in me[1] sometimes just cannot believe we are still using C as the dominant embedded programming languages after all these years, and yet, I also see the simplicity and elegance the C code can bring. After all it’s just a tool, and even a good tool in the wrong hands; well we have plenty of examples of that…
Let’s assume we’ve got a simple task to program; we have a byte-based interface where we’ll receive either command or status messages. The command messages are the byte value 0..3 and the status 10..12. From this we can create a pair of enum’s:
enum { CMD1, CMD2, CMD3, CMD4, END_CMD};
enum { STATUS1 = 10, STATUS2, STATUS3, END_STATUS };
We have three functions to invoke depending on the byte value:
void process_command_msg(int i);
void process_status_msg(int i);
void report_error(int i);
We could start with a if-else-if chain:
if ((num >= CMD1) && (num < END_CMD)) process_command_msg(x); else if ((num >= STATUS1) && (num < END_STATUS)) process_status_msg(x); else report_error(x);
Then refactored to:
#include <stdbool.h> bool valid_command_message(int num) { if ((num >= CMD1) && (num < END_CMD)) return true; return false; } bool valid_status_message(int num) { if ((num >= STATUS1) && (num < END_STATUS)) return true; return false; } void process_message(int x) { if (valid_command_message(x)) { process_command_msg(x); } else if (valid_status_message(x)) { process_status_msg(x); } else { report_error(x); } }
Alternatively, we might consider that for the small set of message types it might be more efficient to use a switch statement:
void process_message(int x) { switch (x) { case CMD1: case CMD2: case CMD3: case CMD4: process_command_msg(x); break; case STATUS1: case STATUS2: case STATUS3: process_status_msg(x); break; default: report_error(x); break; } }
If the implementation uses a form of jump-table then this has the benefit of giving you an O(1) performance based on the messages, whereas with the if-else-if chain, the commands will be checked and processed before status and errors.
The common issue associated with switch statement is typically maintenance; especially where the set of ‘valid’ values needs extending. Let’s assume in v2.0 of the system we want to extend the message set to include two new commands and one new status message; thus:
enum { CMD1, CMD2, CMD3, CMD4, CMD5, CMD6, END_CMD}; enum { STATUS1 = 10, STATUS2, STATUS3, STATUS4, END_STATUS };
The significant difference is, as the code stands, that the if-else-if version will handle the change without modification, whereas the existing switch statement treats the new commands still as errors.
So, could we have structure the switch in a way it could have accommodated these potential changes?
By simply combining the switch and the if-else-if we’d naturally achieve the desired result:
void process_message(int x) { switch (x) { case CMD1: case CMD2: case CMD3: case CMD4: process_command_msg(x); break; case STATUS1: case STATUS2: case STATUS3: process_status_msg(x); break; default: if (valid_command_message(x)) { process_command_msg(x); } else if (valid_status_message(x)) { process_status_msg(x); } else { report_error(x); } break; } }
Okay it works, but it just doesn’t sit right, does it?
Now here it comes; please don’t read on if you have a nervous disposition…
Let’s start by moving the default around:
void process_message(int x) { switch (x) { default: if (valid_command_message(x)) { process_command_msg(x); } else if (valid_status_message(x)) { process_status_msg(x); } else { report_error(x); } break; case CMD1: case CMD2: case CMD3: case CMD4: process_command_msg(x); break; case STATUS1: case STATUS2: case STATUS3: process_status_msg(x); break; } }
We’ve moved the default from the last to the first label; as the switch is a conceptual jump-table this means if x == CMD1 it will bypass the default and jump straight to the case label CMD1.
Now if this means we will always jump to the label, then we can do this:
void process_message(int x) { switch (x) { default: if (valid_command_message(x)) { case CMD1: case CMD2: case CMD3: case CMD4: process_command_msg(x); break; } else if (valid_status_message(x)) { case STATUS1: case STATUS2: case STATUS3: process_status_msg(x); break; } else report_error(x); break; } }
And for the cherry on the top, the else statement (in this context) can act as the equivalent of a break statement. We can, therefore, remove all the break statements (thus also allowing us to remove the need for all the blocks)!
void process_message(int x) { switch (x) default: if (valid_command_message(x)) case CMD1: case CMD2: case CMD3: case CMD4: process_command_msg(x); else if (valid_status_message(x)) case STATUS1: case STATUS2: case STATUS3: process_status_msg(x); else report_error(x); }
At this point I’d expect one of two responses[2]; the majority of you are reaching for a paper bag to help control your breathing and quoting how many MISRA-C rules this single bit of code actually breaks and why we should be using [insert your favorite language here] instead, etc.
But maybe, just maybe, some of you see the beauty…
[1] C.Eng.
[2] Well maybe a 3rd from all the HN trolls who claim they were taught this in kindergarten
Niall Cooling
Niall has been designing and programming embedded systems for over 30 years. He has worked in different sectors, including aerospace, telecomms, government and banking.
His current interest lie in IoT Security and Agile for Embedded Systems.