Как справиться со строгими перечислениями на простом C?

#c

#c

Вопрос:

Взгляните на следующий код:

 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 выводит

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

Это потому, что я могу передать что-то вроде 42 foo , а не только A , B , или C . Итак, вопрос: как сообщить GCC, что только A , B , или C может обрабатываться оператором switch, в противном случае поведение не определено? Приемлема функциональность, зависящая от компилятора.

Позвольте мне указать на некоторые решения, которые меня не удовлетворяют. Во-первых, я мог бы просто вставить, default: __builtin_unreachable(); но это повлияло бы на анализ прецедентов: представьте, что, по-видимому, я добавляю D вариант, и компилятор не скажет мне, что D это необработанный.

Во-вторых, я мог бы вставить if (x > C) { __builtin_unreachable(); } перед оператором switch , но это слишком невозможно, потому switch(x) что на самом деле генерируется макрокомандой, которая не знает о вариантах Foo , она не знает ничего, кроме некоторой переменной x .

В-третьих, я мог бы вставить #pragma GCC diagnostic ignored "-Wreturn-type" , но опять же, switch(x) генерируется макрокомандой, и именно поэтому я не могу вернуть диагностику в предыдущее состояние с помощью #pragma GCC diagnostic pop .

В-четвертых, я мог бы использовать массив вместо switch , но возвращаемые выражения не являются постоянными и предоставляются пользователем, генерирующим макрос switch(x) .

И последнее: я мог бы написать return 42; после оператора switch, но опять же я хочу автоматически отключить предупреждение внутри генерации макроса switch(x) , потому что оно широко используется в моей кодовой базе.

Godbolt

Комментарии:

1. Может быть, добавить default: регистр?

2. @Code-Apprentice: ОП уже озвучил эту идею.

3. Можете ли вы изменить макрос, чтобы присваивать переменную вместо возврата? Затем вы можете вернуть переменную после переключения.

4. @JonathanLeffler, выражения могут быть произвольно сложными и иметь визуальные эффекты, как я указал в вопросе.

5. К сожалению, в C нет концепции строгих перечислений. Вам придется пойти на некоторые уступки.

Ответ №1:

Если вам хочется заняться легким мазохизмом, вы могли бы грубо реализовать поведение, подобное исключению, используя setjmp и 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 = %dn", 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 Foon" );
    }
  }

  return 0;
}
 

Сборки без предупреждений следующим образом (по крайней мере, в моей системе):

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

Вывод:

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