I'm coming back to some C development after working in C++ for a while. I've gotten it into my head that macros should be avoided when not necessary in favor of making the compiler do more work for you at compile-time. So, for constant values, in C++ I would use static const variables, or C++11 enum classes for the nice scoping. In C, static constants are not really compile-time constants, and enums may (? or may not?) behave slightly differently.
So, is it reasonable to prefer using enums for constants rather than #defines?
For reference, here's an excellent list of pros and cons of enums, #defines and static consts in C++.
The TL;DR answer is that it doesn't often matter at all if you use
#define
orenum
.There are however some subtle differences.
The main problem with enums is that you can't change the type. If you use enumeration constants such as
enum { FALSE, TRUE };
, then those constants will always be of typeint
.This might be problematic if you need unsigned constants, or constants of a different size than
sizeof(int)
. Signed integers may cause subtle bugs if you need to do bitwise operations, because mixing those with negative numbers doesn't make any sense in 99% of the cases.With macros however, you can specify any type:
The downside is that you don't have to option to actually define a type with macros, which you could do with enums. enums give a slight bit more of type safety, but only if you typedef them:
typedef enum {FALSE, TRUE} BOOL;
. C doesn't have much type safety at all, but good compilers or external static analysis tools can detect & warn for type issues when trying to convert to/from enum type by accident.Another oddity with that though, is that "BOOL" is an enum variable. enum variables, unlike enum constants, have no guarantee of which integer type they correspond to. You just know that it will be some kind of integer type large enough to fit all values of the corresponding enumeration constants. This might be a very bad thing in case the size of the enum matters.
And of course, enums have the advantage that you can declare them at local scope, so you don't unnecessarily clutter down the global namespace when you don't need to.
If you like. Enums behave like integers.
But I would still prefer constants, instead of both enums and macros. Constants provide type-safety, and they can be of any type. Enums can be only integers, and macros do not respect type safety.
For example :
instead of
or
BTW My answer was related to C++. I am not sure if it applies to C.
The advantage of using
enum { FOO=34 };
over#define FOO 34
is that macros are preprocessed, so in principle the compiler don't really see them (in practice, the compiler does see them; recent GCC has a sophisticated infrastructure to give from what macro expansion some internal abstract syntax tree is coming).In particular, the debugger is much more likely to know about
FOO
fromenum { FOO=34 };
than from#define FOO 34
(but again, this is not always true in practice; sometimes, the debugger is clever enough to be able to expand macros...).Because of that, I prefer
enum { FOO=34 };
over#define FOO 34
And there is also a typing advantage. I could get more warnings from the compiler using
enum color_en { WHITE, BLACK }; enum color_en color;
than usingbool isblack;
BTW,
static const int FOO=37;
is usually known by the debugger but the compiler might optimize it (so that no memory location is used for it; it might be just an immediate operand inside some instruction in the machine code).I've been working in embedded systems for over a dozen years and use C primarily. My comments are specific to this field. There are three ways to create constants that have specific implications for these types of applications.
1) #define: macros are resolved by the C preprocessor before the code is presented to the C compiler. When you look at headers files provided by processor vendors, they typically have thousands of macros defining access to the processor registers. You invoke a subset of them in your code and they become memory accesses in your C source code. The rest disappear and are not presented to the C compiler.
Values defined as macros become literals in C. As such, they do not result in any data storage. There is no data memory location associated with the definition.
Macros can be used in conditional compilation. If you want to strip out code based on feature configuration then you have to use macro definitions. For example:
2) Enumerations: Like macro definitions, enumerations do not result in data storage. They become literals. Unlike macro definitions, they are not stripped by the preprocessor. They are C language constructs and will appear in preprocessed source code. They cannot be used to strip code via conditional compilation. They cannot be tested for existence at compile time or runtime. Values can only be involved in runtime conditionals as literals.
Unreferenced enumerations won't exist at all in compiled code. On the other hand, compilers may provide warnings if enumerated values are not handled in a switch statement. If the purpose of the constant is to produce a value that must be handled logically then only an enumeration can provide the degree of safety that comes with the use of switch statements.
Enumerations also have an auto-increment feature, so if the purpose of the constant is to be used as an constant index into an array then I would always go with an enumeration to avoid unused slots. In fact, the enumeration itself can produce a constant representing a number of items that can be used in an array declaration.
Since enumerations are C language constructs, they are definitely evaluated at compiler time. For example:
CONFIG_BIT_MASK is a text substitute for (1 << CONFIG_BIT_POS). When (1 << CONFIG_BIT_POS) is presented to the C compiler, it may or may not produce the literal 1.
In this case CONFIG_BIT_MASK is evaluated and becomes the literal value 1.
Finally, I would add that macro definitions can be combined to produce other code symbols, but cannot be used to create other macro definitions. That means that if the constant name must be derived then it can only be an enumeration created by a combination of macro symbols or macro expansion, such as with list macros (X macros).
3) const: This is a C language construct that makes a data value read only. In embedded applications this has an important role when applied to static or global data: it moves the data from RAM into ROM (typically, flash). (It does not have this effect on locals or auto variables because they are created on the stack or in registers at runtime.) C compilers can optimize it away, but certainly this can be prevented, so aside from this caveat, const data actually takes up storage in read only memory at runtime. That means that it has type, which defines that storage at a known location. It can be the argument of sizeof(). It can be read at runtime by an external application or a debugger.
These comments are targeted at embedded applications. Obviously, with a desktop application, everything is in RAM and much of this doesn't really apply. In that context, const makes more sense.
I would stick to using the features for their purpose.
A symbolic parameter, taking a discrete value among a set of alternatives, should be represented as an enum member.
A numerical parameter, such as array size or numerical tolerance, should be represented as a const variable. Unfortunately, C has no proper construct to declare a compile-time constant (like Pascal had), and I would tend to say that a defined symbol is equally acceptable. I now even unorthodoxically opt for defined symbols using the same casing scheme as other identifiers.
The enumerations with explicitly assigned values, such as binary masks, are even more interesting. At the risk of looking picky, I would consider to use declared constants, like
This said, I wouldn't care so much about easing the compiler's task when you see how easily they handle the monstrous pieces of code that they receive daily.
A
const int MY_CONSTANT = 7;
will take up storage; an enum or#define
does not.With a
#define
you can use any (integer) value, for example#define IO_PORT 0xb3
With an enum you let the compiler assign the numbers, which can be a lot easier if the values don't matter that much: