One of the least used but potentially most useful C preprocessor directives is #error. Here’s a look at a couple of clever uses for #error that have proven invaluable in embedded software development.
#error is an ANSI-specified feature of the C preprocessor (cpp). Its syntax is very straightforward:
The <writer supplied error message> can consist of any printable text. You don’t even have to enclose the text in quotes. (Technically, the message is optional–though it rarely makes sense to omit it.)
When the C preprocessor encounters a #error statement, it causes compilation to terminate and the writer-supplied error message to be printed to stderr. A typical error message from a C compiler looks like this:
Ennnn: <writer supplied error message>
where Filename is the source file name, line_number is the line number where the #error statement is located, and Ennnn is a compiler-specific error number. Thus, the #error message is basically indistinguishable from ordinary compiler error messages.
“Wait a minute,” you might say. “I spend enough time trying to get code to compile and now he wants me to do something that causes more compiler errors?” Absolutely! The essential point is that code that compiles but is incorrect is worse than useless. I’ve found three general areas in which this problem can arise and #error can help. Read on and see if you agree with me.
I tend to code using a step-wise refinement approach, so it isn’t unusual during development for me to have functions that do nothing, for loops that lack a body, and so forth. Consequently, I often have files that are compilable but lack some essential functionality. Working this way is fine, until I’m pulled off to work on something else (an occupational hazard of being in the consulting business). Because these distractions can occasionally run into weeks, I sometimes return to the job with my memory a little hazy about what I haven’t completed. In the worst-case scenario (which has occurred), I perform a make, which runs happily, and then I attempt to use the code. The program, of course, crashes and burns, and I’m left wondering where to start.
In the past, I’d comment the file to note what had been done and what was still needed. However, I found this approach to be rather weak because I then had to read all my comments (and I comment heavily) in order to find what I was looking for. Now I simply enter something like the following in an appropriate place in the file:
Thus, if I forget that I haven’t done the necessary work, an inadvertent attempt to use the file will result in just about the most meaningful compiler message I’ll ever receive. Furthermore, it saves me from having to wade through pages of comments, trying to find what work I haven’t finished.
As much as I strive to write portable code, I often find myself having to trade off performance for portability – and in the embedded world, performance tends to win. However, what happens if a few years later I reuse some code without remembering that the code has compiler-specific peculiarities? The result is a much longer debug session than is necessary. But a judicious #error statement can prevent a lot of grief. A couple of examples may help.
Some floating-point code requires at least 12 digits of resolution to return the correct results. Accordingly, the various variables are defined as type long double. But ISO C only requires that a long double have 10 digits of resolution. Thus on certain machines, a long double may be inadequate to do the job. To protect against this, I would include the following:
#if (LDBL_DIG < 12)
This approach works by examining the value of an ANSI-mandated constant found in float.h.
An amazing amount of code makes invalid assumptions about the underlying size of the various integer types. If you have code that has to use an int (as opposed to a user-specified data type such as int16), and the code assumes that an int is 16 bits, you can do the following:
#if (INT_MAX != 32767)
Again, this works by checking the value of an ANSI-mandated constant. This time the constant is found in the file limits.h. This approach is a lot more useful than putting these limitations inside a big comment that someone may or may not read. After all, you have to read the compiler error messages.
Since conditionally compiled code seems to be a necessary evil in embedded programming, it’s common to find code sequences such as the following:
As it is written, this code means the following: if and only if OPT_1 is defined, we will do option_1; otherwise we’ll do option_2. The problem with this code is that a user of the code doesn’t know (without explicitly examining the code) that OPT_1 is a valid compiler switch. Instead, the naïve user will simply compile the code without defining OPT_1 and get the alternate implementation, irrespective of whether that is what’s required or not. A more considerate coder might be aware of this problem, and instead do the following:
#elif defined OPT_2
In this case, failure to define either OPT_1 or OPT_2 will typically result in an obscure compiler error at a point later in the code. The user of this code will then be stuck with trying to work out what must be done to get the module to compile. This is where #error comes in. Consider the following code sequence:
#elif defined OPT_2
Now the compilation fails, but at least it tells the user explicitly what to do to make the module compile. I know that if this procedure had been adopted universally, I would have saved a lot of time over the years trying to reuse other people’s code.
So there you have it. Now tell me, don’t you agree that #error is a really useful part of the preprocessor, worthy of your frequent use-and occasional praise?
This article was published in the September 1999 issue of Embedded Systems Programming. If you wish to cite the article in your own work, you may find the following MLA-style information helpful:
Jones, Nigel. “In Praise of the #error Directive” Embedded Systems Programming, September 1999.