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
– ChatGPTenumis a user-defined data type that represents a set of named values. By default, the underlying data type of anenumisint, but it can be explicitly specified to be any integral type using a colon:followed by the desired type.
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…
