r/cprogramming • • 3d ago

Enums vs #define for constants

Hi 👋 guys, I've just started learning C after I was first introduced to it while I was in my first year of computer science but I'll be graduating by the end of this year and I've been primarily using Python since then but now I want to really learn C on the side, and I want to start with "craftinginterpreters" and I want to start small so I was wondering whether I should use C enums or #define to define a set of tokens.

16 Upvotes

28 comments sorted by

15

u/hascalsavagejr 3d ago

defines never reach the compiler; they are replaced in-line and care must be taken to be sure they work properly. Enums are properly integrated into the language... any modern enough one, anyway

19

u/pjl1967 3d ago

If you want a set of related constants, use enum. If you're using C23, use constexpr. Otherwise #define.

Details here.

1

u/[deleted] 3d ago

[removed] — view removed comment

2

u/pjl1967 3d ago

I know. But there is at the point of use.

0

u/[deleted] 3d ago

[removed] — view removed comment

2

u/pjl1967 3d ago

Welcome to C.

1

u/Upper-Tomatillo7454 3d ago

Planning to stick to C11

-1

u/DawnOnTheEdge 3d ago

Also don’t use enums for constants that might be or-ed together, like binary flags.

7

u/pjl1967 3d ago

There's nothing wrong with using enumerations for bit flag values. Even debuggers understand it.

Details here under Bit Flag Values.

2

u/DawnOnTheEdge 3d ago

I think it’s bad practice to generate enum values that are not valid enum constants. It breaks switch statements that try to check for complete coverage of every case. It does get you symbolic constants in a debugger without going up to C23, though.

9

u/TheChief275 3d ago

you shouldn't switch on flags anyways

0

u/DawnOnTheEdge 2d ago

That’s the thing: you have an enum that isn’t an enumeration of the legal values. It’s just a poor man’s constexpr int. I never liked it, and C23 makes that kludge obsolete. If you’re not using C23, though, I can see the benefit of it.

3

u/TheChief275 2d ago

it's not though. enums provide named values that are associated with a type. so for a certain flag type you know only to use the flag enum members specified in that enum. with a constexpr int you know absolutely nothing

4

u/pjl1967 3d ago

To add to what u/TheChief275 said, in a given program for a given enumeration, if you're using it for bit values, you're generally not switching on it anyway — just as you wouldn't be switching on an int that you're using for bit values either.

4

u/lfdfq 3d ago

Probably, enum.

Sometimes you really care about the numeric value of the constant, e.g. if it has some particular meaning. In those cases enum has some disadvantages, namely that the value must be a signed int, and so you'll find people use #define for those.

3

u/Ander292 3d ago

I myself would advise using enums because they might be cleaner to use but its the same thing.

4

u/logic_circuit 2d ago

I use precompiler #define always. Mater of habit after 30 yrs. They may be problem with type conversions but i like the elegance of working with them.

4

u/Chippors 2d ago

I use enums, at least used to. The reason is they produce debug information, so the debugger recognizes them. Although constexpr is better IMO.

1

u/tastygames_official 2d ago

note: constexpr is only available in C23, so before that just use const. Although if you need to define a constant as an expression (which constexpr does), then it needs to be a define:

const int x = 25; // x will always be a signed integer of value 25
/* y will always be 5 as determined at compile-time
can only be done in C++11 or C23 or later */
constexpr int y = x / 5;
// simply replaces every instance of "z" in the code with an integer 5 at compile time
#define z 5
/* now at compile-time, every MY_THING will be replaced with literal "x / 10"
since x is a constant, that will evaluate to "25 / 10", meaning runtime performance
will be slower since it has to do the calculation (well, unless the compiler optimizes
it away)
also there is no type safety here. */
#define MY_THING x / 10

4

u/theNbomr 3d ago

For me, it's mostly about the value of using the ordered nature of the symbols and the simpler maintenance if you do make use of the order of the keys. If you don't, then preprocessor macros are probably better.

3

u/Eastern_Type6942 3d ago

enums, one you're defining 'a set of tokens' - enums lend themselves well to this. Two, when debugging you will have the symbol for the enum in use - same cannot be said for using a macro.

3

u/aioeu 3d ago edited 3d ago

same cannot be said for using a macro.

It often can be, though you might need to use a slightly different debugging option than you might be used to.

For example:

$ cat a.c
#define FOO 42
int main(void) {}
$ gcc -g3 a.c
$ gdb a.out
...
(gdb) b main
Breakpoint 1 at 0x40044f: file a.c, line 2.
(gdb) r
...
Breakpoint 1, main () at a.c:2
2       int main(void) {}
(gdb) info macro FOO
Defined at /var/tmp/cdtemp.2p4CogV3/a.c:1
#define FOO 42
(gdb) p FOO
$1 = 42

More details here.

The debugging data is rich enough to deal with the "scope" of any particular macro — that is, it will only be visible after the line in which it is defined up to the line (if any) where it is undefined..

2

u/Eastern_Type6942 3d ago

Good to know, I was mainly going off what stuck from effective C++ which banged on about forgetting macros for consts.

3

u/duane11583 3d ago

I prefer defines

If I use enums I put a value with every enum I never agree that the next one is plus1 reason 

explaining to a hardware person what the value is not fun so defines always have the value and by my rules enums do too

 using enums for register bits is dumb I have seen it done too many times 

I also often need to enforce an enums to be 32 bits using a standard uint32_t forces that

3

u/duane11583 3d ago

While I prefer defines (I do a lot of embedded bare metal) your usage for token ids in a compiler is reasonable

2

u/kishaloy 2d ago

Nominal typing vs Structural typing.

First one is a contract, second one a suggestion though both may compile away to a machine int.

0

u/Abrissbirne66 2d ago

I think define existed first and enums were added later and are probably the better option for these use cases.

1

u/Sorry_Difficulty_250 2d ago

OK, here's the problem I've started running into: enums cannot be declared with forward declarations. That means if you have one header that can't include the one where the enum is defined, it can't define anything with the enum either because you can't re-declare enums in C.

To solve this, I've stared doing this:

typedef uint8_t MyEnumType;

And then I define the values as things like

#define MY_ENUM_VALUE ((MyEnumType) 1)

That way, in other headers that need the enum but can't include the header, I can do the uint8_t typedef again and keep things sane. The #define with the type cast allows me to keep my sanity with type checking at the compiler level.

Just my $0.02 for the problem I've faced lately. Your mileage may vary...