I may never use a #define in C again. Except when I need to.

I have had a few glorious weeks of C coding at my day job. It is some of the best C code I have ever written, and I am quite proud of it.

During this project, I learned something new. In the past, I would commonly use #define for values such as:

#define NONE 0
#define WARNING 1
#define ERROR 2

There might be some code somewhere that made us of those values:

void HandleError (int error)
{
   case NONE:
      // All fine.
      break;

   case WARNING:
      // Handle warning.
      break;

   case ERROR:
      // Handle error
      break;

    default:
      // Handle unknown error.
      break;
}

When you use #define like that, the C pre-processor will just do simple substitutions before compiling the code — “NONE” will become “0”. Because of this, those labels mean nothing special to the compiler — they are just numbers.

But, I had learned you could use an enum and ensure a function only allows passing the enum rather than “whatever int”:

typedef enum
{
   NONE = 0,
   WARNING = 1,
   ERROR = 2
} ErrorEnum;

Now the routine can take an “ErrorEnum” as a parameter type.

void HandleError (ErrorEnum error)
{
   case NONE:
      // All fine.
      break;

   case WARNING:
      // Handle warning.
      break;

   case ERROR:
      // Handle error
      break;

    default:
      // Handle unknown error.
      break;
}

This looks nicer, since it is clear what is being passed in (whatever “ErrorEnum” is). It does not necessarily give any extra compiler warnings if you pass in an int instead, since an enum is just an int, by default.

In C, an enum is a user-defined data type that represents a set of named values. By default, the underlying data type of an enum is int, but it can be explicitly specified to be any integral type using a colon : followed by the desired type.

– ChatGPT

Thus, the compiler I am using for this test (Visual Studio, building a C program) does not complain if I do something like this:

int error = 1;
HandleError (error);

However, I just learned of an interesting benefit to using an enum for a switch/case. The compiler can warn you if you don’t have a case for all the items in the enum!

typedef enum {
	NONE,
	WARNING,
	ERROR
} ErrorEnum;

void HandleError(ErrorEnum error)
{
	switch (error)
	{
	case NONE:
		// All fine.
		break;

	case WARNING:
		// Handle warning.
		break;

}

Building that with warnings enabled reports:

enumerator 'ERROR' in switch of enum 'ErrorEnum' is not handled	CTest	

How neat! I actually ran in to this when I had added some extra error types, but a routine that converted them to a text string did not have cases for the new errors.

const char *GetErrorString (ErrorEnum error)
{
   const char *errorStringPtr = "Unknown";

   switch NONE:
      errorStringPtr = "None";
      break;
}

That is a useful warning, and it found a bug/oversight.

Because of this, I will never use #defines for things like this again. Not only does a data type look more obvious in the code, the compiler can give you extra warnings.

Until next time…

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.