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

В С++ 03 можно эмулировать строго типизированное перечисление, поместив его в класс (или пространство имен):

struct MyEnum
{
  enum enumName
  {
    VALUE_1 = 1,
    VALUE_2,
  };
};

и использовать его:

MyEnum::enumName v = MyEnum::VALUE_1;

Можно ли сделать что-то подобное в C? Если да, то как?


Я пробовал вот так, но, конечно, это не работает:

struct A
{
  enum aa
  {
    V1 = 5
  };
};

int main()
{
  A::aa a1 = A::V1;
  enum A::aa a2 = A::V1;
  struct A::aa a3 = A::V1;

  return 0;
}

Ответы

Ответ 1

Вот мое решение. Имеет несколько преимуществ перед дизайном @Eric:

  • Поддерживает тестирование равенства (например, A_VALUE_0 == value)
  • Не полагается на составные литералы C99
  • Можно использовать для присвоения неправильного значения перечислению.

Недостатки:

  • Флаги не работают (например, A_VALUE_0 | A_VALUE_1)
  • Не может быть switch 'd
  • Может запутать среду IDE, где строки ошибок при тестировании на равенство (например, A_VALUE_0 == B_VALUE_1)

Примечания:

  • НИКОГДА не разыскивайте указатель этого типа. Вызывает сбои быстрее, чем Lamborghini

Здесь реализация (скомпилирована с -Werror и -pedantic):

typedef struct A { char empty[1]; } *A; // we use 'empty' so that we don't get a warning that empty structs are a GNU extension
#define A_VALUE_0 ((A) 0x1)
#define A_VALUE_1 ((A) 0x2)
#define A_VALUE_2 ((A) 0x4)

typedef struct B { char empty[1]; } *B;

#define B_VALUE_0 ((B) 0x0)
#define B_VALUE_1 ((B) 0x1)
#define B_VALUE_2 ((B) 0x2)

int main()
{
    A a = A_VALUE_0;

    int equal = (a == A_VALUE_1); // works!
    int euqal = (a == B_VALUE_1) // doesn't work

    A flags = A_VALUE_0 | A_VALUE_1; // doesn't work!

    switch (a) { // doesn't work
        case A_VALUE_0:
            puts("value 0");
            break;
        case A_VALUE_1:
            puts("value 1");
            break;
        case A_VALUE_2:
            puts("value 2");
            break;
        default:
            puts("unknown value");
            break;
    } // doesn't work

    // casting works for assignment:
    A b = (A) (B_VALUE_2);

    return 0;
}

Ответ 2

Вы можете сделать это:

// Declare A to use for an enumeration, and declare some values for it.
typedef struct { int i; } A;
#define A0  ((A) { 0 })
#define A1  ((A) { 1 })

// Declare B to use for an enumeration, and declare some values for it.
typedef struct { int i; } B;
#define B0  ((B) { 0 })
#define B1  ((B) { 1 })


void foo(void)
{
    // Initialize A.
    A a = A0;

    // Assign to A.
    a = A1;

    // Assign a value from B to A.
    a = B0; // Gets an error.
}

Это дает вам некоторую типизацию, но это может быть неприятностью, в зависимости от того, какие другие операции вы хотите выполнить с перечислением и его значениями.

Ответ 3

Поскольку C не предоставляет пространства имен, вы можете использовать префиксы вместо этого.

enum MyEnum {
    MyEnumA = 1,
    MyEnumB,
    MyEnumC
};

enum OtherEnum {
    OtherEnumA = 1,
    OtherEnumB
};

Затем, для выполнения конкретизации в объявлениях переменных, вы можете объявлять типы для вашего перечисления, например:

typedef enum MyEnum MyEnum;
typedef enum OtherEnum OtherEnum;

Наконец, я не хочу разрешать неявные преобразования OtherEnumB в тип MyEnum, Clang предоставляет флаг -Wenum-conversion (я не думаю, что в GCC есть аналогичный флаг, к сожалению).

/tmp/test.c:24:20: warning: implicit conversion from enumeration type 'enum OtherEnum' to different enumeration type 'MyEnum' (aka 'enum MyEnum') [-Wenum-conversion]
    MyEnum value = OtherEnumB;
           ~~~~~   ^~~~~~~~~~
1 warning generated.

Это имеет то преимущество, что оно простое, легкое для понимания и хорошо работает с вашим (мое, по крайней мере) автозавершением IDE.