How to deal with strict enumerations in plain C?

Viewed 87

Take a look at the following code:

typedef enum {
    A, B, C
} Foo;

int foo(Foo x) {
    switch(x) {
        case A: return 1;
        case B: return 2;
        case C: return 3;
    }
}

GCC 10.2 outputs

<source>:11:1: warning: control reaches end of non-void function [-Wreturn-type]
   11 | }
      | ^

This is because I can pass something like 42 to foo, not only A, B, or C. So the question is: how to tell GCC that only A, B, or C can be handled by the switch statement, otherwise the behavior is undefined? Compiler-specific functionality is acceptable.

Let me point some solutions that don't satisfy me. First, I could just insert default: __builtin_unreachable(); but this would penetrate case analysis: imagine that apparently I'm adding the D variant and the compiler would not tell me that D is unhandled.

Second, I could insert if (x > C) { __builtin_unreachable(); } before the switch statement, but this is too impossible because switch(x) is actually generated by a macro which doesn't know about the variants of Foo, it knows nothing but some variable x.

Third, I could insert #pragma GCC diagnostic ignored "-Wreturn-type", but again, switch(x) is generated by a macro and this is why I cannot revert the diagnostics to the previous state by #pragma GCC diagnostic pop.

Fourth, I could use an array instead of switch but the returned expressions are not constant and are provided by a user of the macro generating switch(x).

And the last one: I could write return 42; after the switch statement but again I want to disable the warning automatically inside the macro generating switch(x) because it's used extensively in my codebase.

Godbolt

1 Answers

If you feel like engaging in some light masochism, you could crudely implement exception-like behavior using setjmp and longjmp:

#include <stdlib.h>
#include <setjmp.h>
#include <stdio.h>

typedef enum Foo {
  A, B, C
} Foo;

#define ENUM_OUT_OF_RANGE -1

jmp_buf env;

int foo( Foo x )
{
  switch ( x )
  {
    case A: return 1;
    case B: return 2;
    case C: return 3;
    default: longjmp( env, ENUM_OUT_OF_RANGE ); // "throw" ENUM_OUT_OF_RANGE
  }
}

int main( void )
{
  int ex;

  if ( (ex = setjmp( env )) == 0 ) // "try" block
  {
    Foo arr[] = {A, B, C, 42};

    for ( size_t i = 0; i < 4; i++ )
    {
      printf( "value of %d = %d\n", arr[i], foo( arr[i] ) );
    }
  }
  else // "catch" block
  {
    if ( ex == ENUM_OUT_OF_RANGE )
    {
      fprintf( stderr, "foo was called with a value outside the range of Foo\n" );
    }
  }

  return 0;
}

Builds with no warnings as follows (at least on my system):

gcc -o exceptions -std=c11 -pedantic -Wall -Werror exceptions.c

Output:

$ ./exceptions 
value of 0 = 1
value of 1 = 2
value of 2 = 3
foo was called with a value outside the range of Foo
Related