Указание размера типа перечисления в C

Уже прочитал этот связанный вопрос, но искал что-то более конкретное.

  • Есть ли способ сообщить вашему компилятору, какой шире вы хотите видеть enum?
  • Если так, то как ты это делаешь? Я знаю, как это указать в C#; это так же сделано в C?
  • Стоит ли это делать? Когда значение перечисления передается функции, будет ли оно передано как intзначение независимо?

11 ответов

Решение

Есть ли способ сообщить вашему компилятору, какой шире вы хотите видеть enum?

В общем случае нет. Не в стандартном С.

Стоит ли это делать?

Это зависит от контекста. Если вы говорите о передаче параметров в функции, то нет, делать это не стоит (см. Ниже). Если речь идет об экономии памяти при построении агрегатов из перечислимых типов, то, возможно, это стоит сделать. Однако в C вы можете просто использовать целочисленный тип подходящего размера вместо типа enum в агрегатах. В C (в отличие от C++) типы enum и целочисленные типы почти всегда взаимозаменяемы.

Когда значение перечисления передается функции, будет ли оно передано как значение типа int независимо?

Многие (большинство) компиляторы в наши дни передают все параметры как значения естественного размера слова для данной аппаратной платформы. Например, на 64-битной платформе многие компиляторы передают все параметры как 64-битные значения, независимо от их фактического размера, даже если тип int на этой платформе имеется 32 бита (поэтому на такой платформе оно обычно не передается как значение типа int). По этой причине нет смысла пытаться оптимизировать размеры перечислений для целей передачи параметров.

Я считаю, что есть флаг, если вы используете GCC.

-fshort-перечисления

Даже если ты пишешь строго C код, результаты будут зависеть от компилятора. Используя стратегии из этой ветки, я получил несколько интересных результатов...

enum_size.c

#include <stdio.h>

enum __attribute__((__packed__)) PackedFlags {
    PACKED = 0b00000001,
};

enum UnpackedFlags {
    UNPACKED = 0b00000001,
};

int main (int argc, char * argv[]) {
  printf("packed:\t\t%lu\n", sizeof(PACKED));
  printf("unpacked:\t%lu\n", sizeof(UNPACKED));
  return 0;
}
$ gcc enum_size.c
$ ./a.out
packed:         4
unpacked:       4
$ gcc enum_size.c -fshort_enums
$ ./a.out
packed:         4
unpacked:       4
$ g++ enum_size.c
$ ./a.out
packed:         1
unpacked:       4
$ g++ enum_size.c -fshort_enums
$ ./a.out
packed:         1
unpacked:       1

В моем примере выше я не осознавал никакой выгоды от __attribute__((__packed__)) модификатор, пока я не начал использовать компилятор C++.

РЕДАКТИРОВАТЬ:

Подозрение @ технозавра было правильным.

Проверяя размер sizeof(enum PackedFlags) вместо sizeof(PACKED) Я вижу результаты, которые я ожидал...

  printf("packed:\t\t%lu\n", sizeof(enum PackedFlags));
  printf("unpacked:\t%lu\n", sizeof(enum UnpackedFlags));

Теперь я вижу ожидаемые результаты от gcc:

$ gcc enum_size.c
$ ./a.out
packed:         1
unpacked:       4
$ gcc enum_size.c -fshort_enums
$ ./a.out
packed:         1
unpacked:       1

Начиная с C23, это, наконец, возможно в стандартном C:

Вы можете поставить двоеточие и целочисленный тип послеenumключевое слово (или после тега имени, если оно именованное), чтобы указать фиксированный тип нижнего уровня перечисления, который устанавливает размер и диапазон типа перечисления.

Стоит ли это делать? Когда значение перечисления передается функции, будет ли оно независимо передано как значение размера int?

В x86_64 тип целого числа не влияет на то, передается ли оно в регистр или нет (пока оно помещается в один регистр). Однако размер данных в куче очень важен для производительности кэша.

В некоторых случаях это может быть полезно:

typedef uint8_t command_t;
enum command_enum
{
    CMD_IDENT                = 0x00,     //!< Identify command
    CMD_SCENE_0              = 0x10,     //!< Recall Scene 0 command
    CMD_SCENE_1              = 0x11,     //!< Recall Scene 1 command
    CMD_SCENE_2              = 0x12,     //!< Recall Scene 2 command
};

/* cmdVariable is of size 8 */
command_t cmdVariable = CMD_IDENT; 

С одной стороны типа command_t имеет размер 8 и может использоваться для переменных и типов параметров функций. С другой стороны, вы можете использовать перечисляемые значения для присваивания, которые имеют тип int по умолчанию, но компилятор будет разыгрывать их немедленно при назначении command_t переменная типа

Кроме того, если вы делаете что-то небезопасное, например, определение и использование CMD_16bit = 0xFFFF, компилятор предупредит вас следующим сообщением:

предупреждение: большое целое число неявно усекается до типа без знака [-Woverflow]

Существует также другой способ, если перечисление является частью структуры:

struct something {
     :0;
   enum whatever field:CHAR_BIT;
     :0;
};

The:0; может быть опущено, если поле enum окружено обычными полями. Если раньше было другое битовое поле, то:0 принудительно выровняет байты по следующему байту для поля, следующего за ним.

Вы можете заставить его быть по крайней мере определенного размера, определив соответствующее значение. Например, если вы хотите, чтобы ваш enum сохранялся в том же размере, что и intдаже если все значения вписываются в charВы можете сделать что-то вроде этого:

typedef enum {
    firstValue = 1,
    secondValue = 2,

    Internal_ForceMyEnumIntSize = MAX_INT
} MyEnum;

Обратите внимание, однако, что поведение может зависеть от реализации.

Как вы заметили, передача такого значения в функцию в любом случае приведет к его расширению до int, но если вы используете свой тип в массиве или структуре, тогда размер будет иметь значение. Если вы действительно заботитесь о размерах элементов, вы должны использовать такие типы, как int8_t, int32_t, так далее.

Как говорит @Nyx0uf, у GCC есть флаг, который вы можете установить:

-fshort-enums

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

Предупреждение: ключ -fshort-enums заставляет GCC генерировать код, который не двоично совместим с кодом, созданным без этого ключа. Используйте его для соответствия бинарному интерфейсу приложения не по умолчанию.

Источник: https://gcc.gnu.org/onlinedocs/gcc/Code-Gen-Options.html

Дополнительное отличное чтение для общего понимания: https://www.embedded.fm/blog/2016/6/28/how-big-is-an-enum.
Интересно... обратите внимание на линию, которую я выделил желтым цветом ниже!

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

Но я хочу указать на то, что еще не было упомянуто, и для этого я отвечу на третий вопрос так:

Это «стоит сделать» при написании C API, который будет вызываться из языков, отличных от C. Все, что напрямую связано с кодом C, должно правильно понимать структуру памяти всех структур, списков параметров и т. д. в API кода C. К сожалению, типы C, такие как, или, что еще хуже, перечисления имеют довольно непредсказуемый размер (изменения компилятора, платформы и т. д.), поэтому знание структуры памяти всего, что содержит перечисление, может быть хитрым, если только ваш компилятор другого языка программирования также не является компилятором C И у него есть некоторые в языковой механизм для использования этих знаний. Гораздо проще писать без проблемные привязки к библиотекам C, когда API использует типы C предсказуемого размера, такие как,,,,,и т. д., а также структуры/объединения, состоящие из этих типов предсказуемого размера.

Поэтому я бы позаботился о размере перечисления, когда это важно для корректности программы, например, когда возможны проблемы с расположением памяти и выравниванием. Но я бы не стал так сильно беспокоиться об оптимизации, если только у вас не возникнет нишевая ситуация, которая увеличивает альтернативную стоимость (например, большой массив/список значений, типизированных перечислением, в системе с ограниченной памятью, такой как небольшой MCU).

К сожалению, в ситуациях, подобных тем, о которых я упоминаю, не помогает что-то вроде , потому что эта функция зависит от поставщика и менее предсказуема (например, другой системе придется «угадывать» размер перечисления, аппроксимируя алгоритм GCC дляразмер перечисления). Во всяком случае, это позволило бы людям компилировать код C таким образом, который нарушал бы общие предположения, сделанные привязками на других языках (или другой код C, который не был скомпилирован с той же опцией), с ожидаемым результатом повреждения памяти в качестве параметров или члены структуры записываются или считываются из неправильных мест в памяти.

Это зависит от значений, назначенных для перечислений.

Пример: если сохранено значение больше 2^32-1, размер, выделенный для общего перечисления, изменится на следующий размер.

Сохраните значение 0xFFFFFFFFFFFF в переменной enum, оно выдаст предупреждение, если попытается скомпилировать в 32-битной среде (предупреждение о округлении). Где, как и в 64-битной компиляции, это будет успешно, и выделенный размер будет 8 байтов.

Другой способ - привести перечисление внутри объединения следующим образом:

union char_size {
  char a;
  enum {
    a = 1,
    b = 2,
  } val;
};

Это заставит компилятор соответствовать перечислению внутри символа.

Другие вопросы по тегам