Можно ли использовать перечисления вместо #defines для констант времени компиляции в C?

Я возвращаюсь к некоторой разработке C после работы на С++ некоторое время. Я понял, что макросов следует избегать, когда это не нужно, чтобы заставить компилятор работать над вами во время компиляции. Итак, для постоянных значений, в С++ я бы использовал статические константные переменные или классы перечислимого класса С++ 11 для приятного обзора. В C статические константы не являются константами времени компиляции, а перечисления могут (? Или не могут?) Вести себя несколько иначе.

Итак, разумно ли предпочесть использовать перечисления для констант, а не #defines?

Для справки: отличный список плюсов и минусов перечислений, #defines и static consts в С++.

Ответы

Ответ 1

Преимущество использования enum { FOO=34 }; over #define FOO 34 заключается в том, что макросы предварительно обработаны, поэтому в принципе компилятор их действительно не видит (на практике компилятор их видит; последние GCC имеет сложную инфраструктуру, позволяющую получить от какого-то макроразложения некоторое внутреннее абстрактное синтаксическое дерево).

В частности, отладчик гораздо чаще знает о FOO от enum { FOO=34 };, чем от #define FOO 34 (но опять же, это не всегда так на практике, иногда отладчик достаточно умен, чтобы иметь возможность expand macros...).

Из-за этого я предпочитаю enum { FOO=34 }; более #define FOO 34

И есть также печатное преимущество. Я мог получить больше предупреждений от компилятора с помощью enum color_en { WHITE, BLACK }; enum color_en color;, чем использовать bool isblack;

BTW, static const int FOO=37; обычно известен отладчику, но компилятор может его оптимизировать (чтобы для него не использовалось место памяти, оно может быть просто непосредственным операндом внутри некоторой инструкции в машинный код).

Ответ 2

Я хотел бы использовать функции для их целей.

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

Числовой параметр, такой как размер массива или числовой допуск, должен быть представлен как константная переменная. К сожалению, у C нет надлежащей конструкции для объявления константы времени компиляции (как у Паскаля), и я хотел бы сказать, что определенный символ одинаково приемлем. Я теперь даже неортодоксически выбираю определенные символы, используя ту же схему обсадной колонны, что и другие идентификаторы.

Перечисления с явно назначенными значениями, такими как двоичные маски, еще более интересны. Рискуя выглядеть придирчивым, я бы подумал использовать объявленные константы, например

#define IdleMask 1
#define WaitingMask 2
#define BusyMask (IdleMask | WaitingMask)
enum Modes { Idle= IdleMask, Waiting= WaitingMask, Busy= BusyMask };

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

Ответ 3

разумно ли предпочесть использовать перечисления для констант, а не #define?

Если хочешь. Перечисления ведут себя как целые числа.

Но я бы предпочел константы, а не как перечисления, так и макросы. Константы обеспечивают безопасность типа, и они могут быть любого типа. Перечисления могут быть только целыми числами, а макросы не учитывают безопасность типов.

Например:

const int MY_CONSTANT = 7;

вместо

#define MY_CONSTANT 7

или

enum
{
  MY_CONSTANT = 7
};

BTW Мой ответ был связан с С++. Я не уверен, относится ли это к C.

Ответ 4

A const int MY_CONSTANT = 7; займет хранение; перечисление или #define не работает.

С помощью #define вы можете использовать любое (целочисленное) значение, например #define IO_PORT 0xb3

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

enum {
   MENU_CHOICE_START = 1,
   MENU_CHOICE_NEXT,
   ...
};

Ответ 5

Я работаю во встроенных системах уже более десяти лет и использую C в первую очередь. Мои комментарии относятся к этой области. Существует три способа создания констант, которые имеют специфические последствия для этих типов приложений.

1) #define: макросы разрешаются препроцессором C до того, как код будет представлен компилятору C. Когда вы просматриваете файлы заголовков, предоставляемые поставщиками процессоров, у них обычно есть тысячи макросов, определяющих доступ к регистрам процессора. Вы вызываете подмножество из них в своем коде, и они становятся образами доступа к памяти в исходном коде C. Остальные исчезают и не представлены компилятору C.

Значения, определенные как макросы, становятся литералами в C. По сути, они не приводят к хранению данных. Нет описания памяти данных, связанной с определением.

Макросы могут использоваться в условной компиляции. Если вы хотите разбить код на основе конфигурации функций, вам нужно использовать определения макросов. Например:

#if HEARTBEAT_TIMER_MS > 0
    StartHeartBeatTimer(HEARTBEAT_TIMER_MS);
#endif

2) Перечисления. Как и определения макросов, перечисления не приводят к хранению данных. Они становятся литералами. В отличие от макроопределений, они не разделяются препроцессором. Они являются конструкторами языка C и появятся в предварительно обработанном исходном коде. Они не могут использоваться для кодирования кода с помощью условной компиляции. Они не могут быть проверены на наличие во время компиляции или времени выполнения. Значения могут использоваться только в условных выражениях времени выполнения как литералы.

Необязательные перечисления вообще не будут существовать в скомпилированном коде. С другой стороны, компиляторы могут предоставлять предупреждения, если перечисленные значения не обрабатываются в инструкции switch. Если целью константы является создание значения, которое должно обрабатываться логически, то только перечисление может обеспечить степень безопасности, которая приходит с использованием операторов switch.

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

Поскольку перечисления являются конструкциями языка C, они определенно оцениваются во время компилятора. Например:

#define CONFIG_BIT_POS 0
#define CONFIG_BIT_MASK (1 << CONFIG_BIT_POS)

CONFIG_BIT_MASK является заменой текста для (1 < CONFIG_BIT_POS). Когда (1 < CONFIG_BIT_POS) представляется компилятору C, он может или не может выдавать литерал 1.

enum {
    CONFIG_BIT_POS = 0,
    CONFIG_BIT_MASK = (1 << CONFIG_BIT_POS)
};

В этом случае CONFIG_BIT_MASK оценивается и становится литеральным значением 1.

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

3) const: это C-языковая конструкция, которая делает значение данных только для чтения. Во встроенных приложениях это играет важную роль при применении к статическим или глобальным данным: оно перемещает данные из ОЗУ в ПЗУ (обычно, вспышка). (Это не влияет на локальные или автоматические переменные, поскольку они создаются в стеке или в регистре во время выполнения.) Компиляторы C могут оптимизировать его, но, конечно, это можно предотвратить, поэтому, помимо этого оговорки, данные const фактически хранящегося в памяти только для чтения во время выполнения. Это означает, что у него есть тип, который определяет это хранилище в известном месте. Это может быть аргумент sizeof(). Его можно прочитать во время выполнения внешним приложением или отладчиком.

Эти комментарии предназначены для встроенных приложений. Очевидно, что с настольным приложением все в ОЗУ, и большая часть этого на самом деле не применяется. В этом контексте константа имеет смысл.

Ответ 6

Ответ TL, DR заключается в том, что это не часто имеет значения, если вы используете #define или enum.

Однако есть некоторые тонкие различия.

Основная проблема с перечислениями заключается в том, что вы не можете изменить тип. Если вы используете константы перечисления, такие как enum { FALSE, TRUE };, то эти константы всегда будут иметь тип int.

Это может быть проблематично, если вам нужны неподписанные константы или константы другого размера, чем sizeof(int). Подписанные целые числа могут вызывать тонкие ошибки, если вам нужно выполнять побитовые операции, потому что смешивание с отрицательными числами не имеет смысла в 99% случаев.

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

#define X 0                 // int type
#define X 0u                // unsigned int type
#define X 0ul               // unsigned long type
#define X ((uint8_t)0)      // uint8_t type

Недостатком является то, что у вас нет необходимости определять тип с помощью макросов, которые вы могли бы сделать с перечислениями. перечисления дают немного больше безопасности типа, но только если вы их напечатали: typedef enum {FALSE, TRUE} BOOL;. C не имеет большого уровня безопасности вообще, но хорошие компиляторы или внешние инструменты статического анализа могут обнаруживать и предупреждать о проблемах типа при попытке конвертировать в/из типа перечисления случайно.

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

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

Ответ 7

В настоящее время в C++ нет реальной веской причины использовать #define для констант времени компиляции. С другой стороны, есть веские причины использовать взамен enum или enum class. Первое и самое важное - они намного более читабельны при отладке.

В C вы можете явно выбрать базовый тип, что невозможно с enum s. Это может быть причиной для использования define или const. Но перечисления должны быть сильно предпочтены.

Затраты времени выполнения не являются проблемой - в современных компиляторах не будет никакой разницы в генерируемом машинном коде (пока sizeof(the_enum)=sizeof(the_type_used_by_define_based_values)).