Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Make compiler assume that all cases are handled in switch without default

Let's start with some code. This is an extremely simplified version of my program.

#include <stdint.h>

volatile uint16_t dummyColorRecepient;

void updateColor(const uint8_t iteration)
{
    uint16_t colorData;
    switch(iteration)
    {
    case 0:
        colorData = 123;
        break;
    case 1:
        colorData = 234;
        break;
    case 2:
        colorData = 345;
        break;
    }
    dummyColorRecepient = colorData;
}

// dummy main function
int main()
{
    uint8_t iteration = 0;
    while (true)
    {
        updateColor(iteration);
        if (++iteration == 3)
            iteration = 0;
    }
}

The program compiles with a warning:

./test.cpp: In function ‘void updateColor(uint8_t)’:
./test.cpp:20:25: warning: ‘colorData’ may be used uninitialized in this function [-Wmaybe-uninitialized]
     dummyColorRecepient = colorData;
     ~~~~~~~~~~~~~~~~~~~~^~~~~~~~~~~

As you can see, there is an absolute certainty that the variable iteration is always 0, 1 or 2. However, the compiler doesn't know that and it assumes that switch may not initialize colorData. (Any amount of static analysis during compilation won't help here because the real program is spread over multiple files.)

Of course I could just add a default statement, like default: colorData = 0; but this adds additional 24 bytes to the program. This is a program for a microcontroller and I have very strict limits for its size.

I would like to inform the compiler that this switch is guaranteed to cover all possible values of iteration.

like image 364
NO_NAME Avatar asked Jan 11 '19 14:01

NO_NAME


1 Answers

As you can see, there is an absolute certainty that the variable iteration is always 0, 1 or 2.

From the perspective of the toolchain, this is not true. You can call this function from someplace else, even from another translation unit. The only place that your constraint is enforced is in main, and even there it's done in a such a way that might be difficult for the compiler to reason about.

For our purposes, though, let's take as read that you're not going to link any other translation units, and that we want to tell the toolchain about that. Well, fortunately, we can!

If you don't mind being unportable, then there's GCC's __builtin_unreachable built-in to inform it that the default case is not expected to be reached, and should be considered unreachable. My GCC is smart enough to know that this means colorData is never going to be left uninitialised unless all bets are off anyway.

#include <stdint.h>

volatile uint16_t dummyColorRecepient;

void updateColor(const uint8_t iteration)
{
    uint16_t colorData;
    switch(iteration)
    {
    case 0:
        colorData = 123;
        break;
    case 1:
        colorData = 234;
        break;
    case 2:
        colorData = 345;
        break;

    // Comment out this default case to get the warnings back!
    default:
        __builtin_unreachable();
    }
    dummyColorRecepient = colorData;
}

// dummy main function
int main()
{
    uint8_t iteration = 0;
    while (true)
    {
        updateColor(iteration);
        if (++iteration == 3)
            iteration = 0;
    }
}

(live demo)

This won't add an actual default branch, because there's no "code" inside it. In fact, when I plugged this into Godbolt using x86_64 GCC with -O2, the program was smaller with this addition than without it — logically, you've just added a major optimisation hint.

There's actually a proposal to make this a standard attribute in C++ so it could be an even more attractive solution in the future.

like image 194
Lightness Races in Orbit Avatar answered Dec 10 '22 01:12

Lightness Races in Orbit