‘Abusing’ the C switch statement – beauty is in the eye of the beholder

Copyright: <a href='https://www.123rf.com/profile_stocksnapper'>stocksnapper / 123RF Stock Photo</a>

The C Language

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…

To the task ahead

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.

Adaptability

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?

Accommodating change

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?

Abuse ahoy

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);
}

Finally…

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

Director at Feabhas Limited
Co-Founder and Director of Feabhas since 1995.
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.
Dislike (11)

About Niall Cooling

Co-Founder and Director of Feabhas since 1995. 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.
This entry was posted in C/C++ Programming and tagged , , . Bookmark the permalink.

29 Responses to ‘Abusing’ the C switch statement – beauty is in the eye of the beholder

  1. Use a function-pointer jump table instead. Check x is in bounds, then jump. Compile-time calculation always better than run-time.

    Like (10)
    Dislike (5)
  2. Yes of course, the idea wasn't necessarily to come up with an optimal solution (I'd never write it this way myself, it's example of code I was exposed too), the intent was to demonstrate aspects of the switch statement that some C programmers may not be aware of. Thanks for the comment.

    Like (6)
    Dislike (20)
  3. I made a new language from God called "HolyC" like the Ten Commandments and wrote a compiler being God's avatar with divine intellect.

    Like (18)
    Dislike (7)
  4. Paul R Potts says:

    I'm grinning... because (1) yes, this is very clever, and I'm at least moderately sure it is standards-compliant, and (2) no, I would never, ever use this in production code... not because I'm necessarily following a specific standard such as MISRA, but because I like to reduce the number of swear words uttered when reviewing code. Obligatory XKCD: https://xkcd.com/1695/

    Like (6)
    Dislike (4)
  5. saurik says:

    I think this looks a *lot* better if you use "if (false);" at the top to normalize the "else if" in the rest of the chain.

    > switch (x)
    > default: if (false);
    >
    > else 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);

    Like (3)
    Dislike (2)
  6. admin says:

    Nice.

    Like (0)
    Dislike (1)
  7. Don J. Slogar says:

    what about an alternate for the case that models the first refactor if statement, then there is no need to make updates when the enums change and there is no ambiguity and it is short and sweet.

    void process_message(int x)
    { switch (x)
    {case: valid_command_message(x)
    process_command_msg(x);
    break;
    case: valid_status_message(x)
    process_status_msg(x);
    break;
    default: report_error(x);
    break;
    }
    }

    Like (0)
    Dislike (1)
  8. KSt says:

    Reminds me to Duff's device. See https://en.m.wikipedia.org/wiki/Duff's_device

    Like (1)
    Dislike (0)
  9. Well, I'll be the devils advocate here. Isn't this is just another obscure point to make that will take more time then necessary and have maintenance scratching their heads? I thought we, as senior IT thinkers, are solving real-world problems today that already have tight timelines? I can already see this as a distraction to the time box of a group code review. I do not deny it will work, but this isn't the way to mentor lesser experienced developers about functional obscurity. You'd likely have to explain the code segment to every single person that is assigned to existing code like this. Well, thanks for the info, but hey, don't lead new, or mid-level developers so far out of scope as to think about this unless they know they are processing some massive block of data. I apologize, I ramble here. "Cute" as this is, it is not "Elegant" like you might seem to think it is. Nor is it nearly the fastest algorithm to accomplish scaling for high availability. But hey, it's worth a moment consideration. Thank you for that.

    Like (3)
    Dislike (0)
  10. The fewer people that know that this abomination is possible, the better. 🙂

    The ONLY possible excuse would be if this switch was using 10% of your execution time, in which case, I would expect a lot more comments.

    Like (2)
    Dislike (1)
  11. Gene Ognibene says:

    down and dirty

    Like (0)
    Dislike (0)
  12. Nir Friedman says:

    It's much simpler to simply take the original, get rid of the default case in lieu of setting some variable to show that a correct case was successfully entered, and then use the compiler warning which forces you to handle enums exhausively. Then when you add enum values, you get compiler errors that you are forced to correct. This is much better than silently degrading performance when you add new enum values.

    Like (3)
    Dislike (1)
  13. JerryS says:

    Better yet - "cmd" and "status" are two different concepts. "num" should not be used for both of them. It just complicates not only this code, but the code calling these functions.

    Like (4)
    Dislike (0)
  14. sfossen says:

    Anyone else think returning the boolean statement is cleaner than if(true) return true; return false;

    bool valid_status_message(int num) {
    return ((num >= STATUS1) && (num < END_STATUS));
    }

    and both are cleaner than putting in the else.

    Like (3)
    Dislike (0)
  15. hacksoncode says:

    While this is ok, technically, you've traded one set of maintenance headaches for another.

    Let's imagine someone decides they want to do a debug output on command messages, and replaces the first "if" with:

    bool bCmd = valid_command_message(x);
    if (bCmd) ... // the rest is the same
    ...
    DebugOutput(bCmd);

    Very subtle bug.

    Like (1)
    Dislike (0)
  16. Craig says:

    If the functions are defined to return an integer, then can simplify/clarify the above
    by making use of the short-circuit evaluation of C's conditional logic operators to do the switching
    and use the if...else command to make it human presentable.
    Save the switch statement for altering the flow of the program based on what happened while fetching/processing the command/status data. e.g. continue the status/command loop;
    exit the status/command loop; or terminate the program.
    This approach at least permits the ability to handle program continuation/termination based
    on the results of all of the functions used (message, status, and/or process).
    Wonky flow, but is clear, up until the switch statement what's going on.

    enum { EXIT = 0, ERROR, CONTINUE, NEXT_CMD, NEXT_MSG }
    int results = EXIT; // Keep going?

    do {

    results=fetch_x(x); // function to get the info for "x"

    /*
    * Go "process the data"
    */

    // Is something to process a cmd message -> attempt to process it
    if ( (!results) && (valid_command_message(x)) && (results=process_command_msg(x)))

    // Is something to process a status message -> attempt to process it
    else if ( (!results) && (valid_status_message(x)) && (results=process_status_msg(x)))

    // handle the unknown
    else results = ERROR;

    /*
    * Based on results of data processed, how does the program proceed
    */

    // where do we go next? -- continue processing data or move on to something else
    switch (results)
    {

    // stop processing data; continue one with rest of program
    // handle the unknown; ERROR just causes status/command processing to stop
    ERROR: report_error(x);
    EXIT:
    default : print("Command/status processing complete!")
    break; //exit switch(results) && by default do...while(1)

    // continue processing data e.g. restart loop
    // move ERROR here if want to continue processing status/commands
    PROCEED: NEXT_CMD: NEXT_MSG:
    continue; // go start the do...while(1) loop
    break; //never reach this statement!
    /*
    // This is the spot to add ERROR with a function to check to see if the error can be just noted
    // and continue on with status/command processing; stop doing status/command
    // processing and move on with rest of program; OR exit the program!
    ERROR :
    {
    int what2do = error_status(results);
    switch (what2do)
    {
    default: ABORT: exit_routine(); // fatal error, clean things up and terminate
    RESUME: continue; break; // restart do...while(1) loop
    }
    }
    break; // exit switch(results) && do...while(1)
    // e.g. stop processing status/command data and do rest of program
    */

    /*
    // Put default here to catch what shouldn't ever happen, vs. just play dumb above!
    // default shouldn't happen, but one never knows!
    default : print("Unexpected command/status processing path!\n");
    print("Continuing on with rest of program!\n");
    break; //exit switch(results) && do...while(1)
    */

    } // end of switch(results)

    break; // leave the while loop any time we exit switch(results) and
    // continue on with what's after the while(1)!

    } while (1); // never reach this statement!

    /*
    * Rest of program here!
    */

    Like (0)
    Dislike (0)
  17. Matt says:

    If you don't know it, take a look at Duff's device:
    https://en.wikipedia.org/wiki/Duff's_device
    Pure poetry, once/if you stop twitching after reading it.

    Like (0)
    Dislike (0)
  18. This has it's appeal... although you may never get it through a review for production code... but you should know your language 😉

    Btw, this really reminds me after the Duff's Device (https://en.wikipedia.org/wiki/Duff's_device), which you probably already knew.

    Like (0)
    Dislike (0)
  19. Jan says:

    Duff's device, anyone? 🙂

    While I might see some beauty in it, the code smell increases with each update of the command set. And if you have to touch the code anyway, you can still go with the former solutions.

    (I still don't understand why somebody "dislikes" every comment in this article -.-)

    Like (0)
    Dislike (1)
  20. dascandy says:

    Didn't we give this job to the compiler like 20 years ago?

    Like (0)
    Dislike (0)
  21. admin says:

    Yes, I think many are familiar with Duff's it's a mainstay in the embedded world, but obviously uses a loop rather than the if-else-if as show here, but in the same vain.

    Like (1)
    Dislike (0)
  22. admin says:

    Yes I was taught Duff's many, many years ago. Duff's had a very clear role and purpose (I'm not so sure this code does 😉

    Like (0)
    Dislike (0)
  23. admin says:

    I probably regret that bit of code as it was the emphasis, when I first wrote the posting I only had the prototypes and maybe I should have left it at that. There's a whole new debate there about the 'clean' way to return, and using structure programming there should only ever be one return statement (I was being lazy!)

    Like (0)
    Dislike (0)
  24. mrkkrj says:

    Bjarne S. used is too for a Duff's Device likish generic type switch implementation:

    typedef decltype(x) T;
    static std::unordered map⟨T,size t⟩ jump targets;

    switch (size t& jump to = jump targets[x]) {
    default: // entered when we have not seen x yet
    if (P1(x)) { jump to = 1; case 1: s1;} else
    if (P2(x)) { jump to = 2; case 2: s2;} else
    ...
    if (Pn(x)) { jump to = n; case n: sn;} else
    jump to = n + 1;
    case n + 1: // none of the predicates is true on x
    }

    Like (0)
    Dislike (0)
  25. M G says:

    This is fantastic...not because of the code, but because I've finally just grasped Duff's Device for the first time! Thank you!!!!

    Like (0)
    Dislike (0)
  26. Craig says:

    No, the example is NOT production code.

    "... should only ever be one return statement ..." needs a context.

    The switch/break/continue/longjump/duff stuff is need order to do/implement coroutines / generator functions in C without resorting to "pure" assembly at the expense of readability/maintainability. Obviously trying to do OS level stuff at user/program level is NOT a good idea for general purpose programs. Since this is a C language extension, should really be
    done in a library. (see jumptable comments below)

    Duff's device is a lot cleaner than the equivalent without duff's device -> http://gist.github.com/Segmented/e2e6b110ae070ef402f0

    The example of trying to OS stuff in program space : http://fanf.livejournal.com/105413.html

    Likely example intended at the start of this article : http://www.chiark.greenend.org.uk/~sgtatham/coroutines.html Although if C treated functions as 1st class (e.g. usable on either side of the assignment statement) then could write the above in a single readable line where program alternates between status, command, error.

    Instead of a jumptable, a simple assembly-language stack-switching library would let you give up control anywhere and not at the top-level routine. Obviously, using assembly introduces portability issues. Cello has plenty of C hacked to the max examples used to extend C to allow corountines/functions as first class. http://libcello.org/

    Like (0)
    Dislike (1)
  27. Craig says:

    http://www.chiark.greenend.org.uk/~sgtatham/mp/ has a discussion on using the C preprocessor to define coustom looping and control constructions.

    Like (0)
    Dislike (1)
  28. Neil Harding says:

    I like it, I had a preprocessor for Java that allowed you to mix bytecode in with regular Java (it was to reduce code size on J2ME games, so 64K Jar was the limit including graphics). One nice trick I had was goto(5) which inside a switch statement would allow you to branch to another case.

    switch(var) {
    case 2:
    someCode();
    goto(4);
    case 3:
    someOtherCode();
    case 4:
    finalCode();
    }

    The best optimization was the 0 byte instructions, so char int2char(int n) {return char(n);} which allowed a 0 byte cast instruction when you knew the result would fit in the desired size.

    Like (0)
    Dislike (1)
  29. numeja says:

    There is only one sane way to do this. Which is the simplest that can be explained; KISS.

    To my mind it is a switch statement with a specific entry for every entry in the two enums. The compiler can optimise it to its heart's content.

    Change the enums, change the switch statements. Leave a trail in the comment by the enum and the code.

    If you ever tried to do pair programming on any of these 'software craftsmanship' solutions it would be thrown out because they take far too long to explain.

    Like (0)
    Dislike (1)

Leave a Reply