When building someone else's software, there's nothing more demoralizing than seeing whole rafts of compiler warnings fly by. A common reaction to such spewage is, "Are these real issues requiring investigation, or is this just the handiwork of a sloppy developer?" Not all warnings indicate problems with code, but there's no way to know for certain without actually looking at each and every warning. Needless to say, when warnings turn out to be nothing, it's downright frustrating -- and a huge waste of time. So, professional developers not only work hard to eliminate compiler warnings, they ask the compiler to produce more of them. Why? There are three very good reasons:
When building someone else’s software, there’s nothing more demoralizing than seeing whole rafts of compiler warnings fly by. A common reaction to such spewage is, “Are these real issues requiring investigation, or is this just the handiwork of a sloppy developer?” Not all warnings indicate problems with code, but there’s no way to know for certain without actually looking at each and every warning. Needless to say, when warnings turn out to be nothing, it’s downright frustrating — and a huge waste of time. So, professional developers not only work hard to eliminate compiler warnings, they ask the compiler to produce more of them. Why? There are three very good reasons:
1. Sometimes the extra warnings represent real issues that wouldn’t have been reported by a more lenient set of diagnostics. Never turn down the compiler’s help. It’s the cheapest way to find bugs.
2. By eliminating all warnings, you can’t help but notice when a new warning appears. Otherwise, your build with twenty “Oh, just ignore those” warnings could easily hide a twenty-first warning that’s actually meaningful.
3. When the next person (or perhaps even you, six months from now) builds your code — especially on a different platform — they won’t waste time tracking down “unimportant” warnings from the compiler.
In this month’s column, let’s talk about clean compiles: how to turn on warnings and attend to them properly to produce diagnostic-free (and perhaps even “bug-free”) code. All of the examples here use the GNU compiler, but the techniques shown apply generally to all compilers.
Turn on Da Noise
In normal operation, a compiler generates a diagnostic only when it finds a problem that actually prevents the program from compiling. However, virtually every compiler can produce more diagnostics upon request.
To generate as much feedback as possible, let’s add these flags to the CFLAGS value in our Makefiles:
CFLAGS = -W -Wall -pedantic
The compiler man pages describe the flags in more detail, but suffice it to say that these options work together to produce much more substantial diagnostic output. There are times when warnings shouldn’t be fully enabled — typically when working on old code that’s just too much work to retrofit — but for new code, you should always ask for everything.
Let’s look at the first example. Here’s a very simple C subroutine called testfunction() in file test1.c:
1: // test1.c
5: printf(“hello, world\n”);
To compile this code, use gcc with the “noisy” diagnostics enabled:
% gcc -W -Wall -pedantic -c test1.c
test1.c:1:1: warning: C++ style comments
are not allowed in ISO C89
test1.c:1:1: warning: (this will be
reported only once per input file)
test1.c:4: warning: return type defaults
test1.c: In function ‘testfunction’:
test1.c:5: warning: implicit declaration
of function ‘printf’
test1.c:6: warning: control reaches end
of non-void function
That’s quite a lot of warnings for just four lines of code. Let’s see what each one means.
Line 1 produces C++ style comments are not allowed in ISO C89. It’s never been legal to use C++-style comments in C code, though many compilers allow it as a courtesy. Enough compilers reject this out of hand that it’s wise to stick solely with C-style comments.
The warning return type defaults to ‘int’ associated with line four is caused by the lack of an explicitly declared return type for testfunction(). In the old days, functions that had no explicit return type defaulted to int, but the lack of a return type was often used as a synonym for “no return at all.” This was before the void type was introduced. However, in modern code, there is no reason to omit void. Pick void or int as needed. It makes the code perfectly clear, and silences the compiler.
On line 5, a warning like implicit declaration of function ‘printf’ occurs whenever the compiler sees a function call before the function has been declared. This is technically legal, but is a really bad idea. Why? Because the compiler assumes — perhaps incorrectly — that the return type is int, and has no knowledge of the parameter types.
Instead, for library functions such as printf(), the solution is to find the correct header file (here, <stdio.h>) and include it. Private (static) functions defined after the use of the function file can be declared at the top of the source file like this:
static void foo(int a, char *b);
However, functions found in other modules should not be done this way, even though it’s superficially appealing. Functions in other external modules should always appear in a header file so that all callers see the same, single declaration, thereby avoiding a maintenance problem should the function type change in the future. More importantly, the module itself should include its header file too, so that the declaration in the header “compares” with the actual definition. The comparison is the only way to catch a mismatch between a prototype and a formal definition. Otherwise, the error is very hard to track dcown.
What’s happening on line 6? What does control reaches end of non-void function mean? Recall that testfunction()‘s return type defaulted to int because type was omitted. But this function doesn’t actually return anything — it just falls off the bottom, providing random garbage to any calling function that’s looking at the return value. If a function really should return a value, one should be provided in an explicit return statement. But if there’s no real return value, then it should be typed void.
Given a warning about incompatible types, one of your first reflexes may be to use a cast, but this isn’t always the right approach. There are two broad kinds of casts: “trust me” casts, and casts that actually force a change in data representation. It’s the former that causes real concern. Take a look at this fragment:
1: short foo(void)
3: return 1000000;
% gcc -c -W test.c
test.c: In function ‘foo’:
test.c:3: warning: overflow in implicit constant conversion
Since under no circumstance can a short represent anything larger than 32,767, a million just can’t fit and the compiler properly warns you about it. But if you change the code to…
return (short) 1000000;
…the compiler clearly knows how to truncate a bigger number into a smaller variable, so the cast isn’t providing any useful technical information. Instead, the cast is a “trust me,” allegedly indicating that the developer knows what he or she is doing. Sure, it suppresses the warning, but it doesn’t magically make a 20-bit number fit into a 16-bit variable. The code is still probably wrong, but the compiler no longer warns you.
As Henry Spencer once said, “If you lie to the compiler, it will get its revenge.”
Dealing with Unused Variables
Two particular warnings, unused variables and unused parameters, show up now and then, and with a little effort can also be addressed. Let’s look at unused parameters first. Here’s another code snippet, test2.c, and the command to compile it:
1: #include <stdio.h>
3: void myfunction2(int a, int b)
5: printf(“a = %d\n”, a);
% gcc -c -W -Wall -pedantic-errors \
test2.c: In function ‘myfunction2′:
test2.c:3: warning: unused parameter ‘b’
The warning unused parameter ‘b’ highlights one of three possible oversights:
1. The parameter int b is mistakenly not used by the subroutine. If this turns out to be the actual error, it represents a genuine bug that the compiler found for you.
2. The parameter int b is truly superfluous, and can be removed outright. Removing the parameter must be done here in the function definition, in the prototypes in the header files, and in all of the places in the code that call the function. This isn’t a bug, but “fixing” it is a real code cleanup.
3. The parameter is unused in some or in all cases, but can’t be removed. This can happen when the function must fit a certain template because its address is taken (such as a signal handler), or when the body of the function is conditionally compiled to use the parameter on some platforms, but not on others.
In the last case, there is nothing really to “fix” — the code is doing what it’s supposed to — so the question is how to make the warning go away. For the latter case we turn to a helper macro, which should be defined in a common header file shared by all modules:
#define UNUSED_VARIABLE(x) ((void) (x))
This macro can be referenced in a function that has variables or parameters that aren’t used, as the (void) cast effectively “uses” the variable in way that suppresses the warnings.
void signal_handler(int signo)
/* process the signal here */
for (i = 0; i < MAX; i++)
The benefits of this simple macro go beyond just suppressing the warning (though it clearly drives its use). The macro also serves as documentation of the intent of the programmer, which will benefit some future maintainer who won’t mistake it for a sloppy bug.
As hard as you may try, not every warning can be suppressed. Indeed, sometimes bugs in the compiler generate spurious warnings. It’s not worth contorting correct code to make a bogus warning go away for a buggy platform. In these kinds of instances, write a short note in a README explaining that the warning is unavoidable.
And on those very rare occasions when a warning simply cannot be suppressed, it’s imperative to make a note in the code itself to guide the developer. For example, in the sample code in June’s “Compile Time” column (available online at http://www.linux-mag.com/2003-06/compile_01.html), the myassert.c file contained a function marked “this never returns,” but it actually did — and generated a diagnostic.
/* NOTE: this function is marked (noreturn), and the compiler
* complains that this function falls off the bottom. The compiler
* is strictly correct, but in the big picture, assertions DON’T
* return, so we really need to keep the __attribute__((noreturn))
* on this function.
* myassert.c:71: warning: ‘noreturn’ function does return
* So we’ll get a warning every time that we don’t know how to
* suppress. Sorry :-(
This comment not only warns a future maintainer of the code that the warning is expected, but it might even provoke someone who knows how to fix it to do so.
Historically, some of the most difficult warnings to address are the conflicts between signed and unsigned variables. In fact, many of those kinds of warnings are ignored even by experienced developers. Regretfully, it’s an intricate subject that requires much more exposition than we can provide here. But we can provide a brief overview.
There are three sorts of sign errors that come up again and again:
1. Numeric Representation
In addition to the obvious fact that you can’t park a negative number in an unsigned variable, the converse is true as well. If you look at the ranges of 32-bit signed and unsigned integers, you notice that there are large areas of non-overlap:
32-bit signed int -2147483648 .. 2147483647
32-bit unsigned int 0 .. 4294967295
So, for instance, trying to assign the value 3,000,000,000 to a signed integer leads to the value -1294967296 in the target. This might qualify as a “surprise.” Make sure you know the ranges of variables when mixing signed and unsigned.
2. “The Usual Conversions”
When two integral values are involved in a binary operation (addition, greater-than, etc.), the compiler automatically adjusts both operands so they ultimately have identical types. The rules for this are tricky and have all kinds of “depends on” factors. For example, any operation involving unsigned char and short operands converts both to int under ISO C but to unsigned int in traditional C.
We’ve written a tool that accepts two data types and shows the conversions applied (including at the intermediate level) with all of the “depends on” factors included. You can try this tool at http://www.unixwiz.net/usualconversions.cgi. The results might surprise you.
3. Characters Types
For historical reasons, char variables are not defined by the language as signed or unsigned: it’s up to the compiler writer to choose what’s most efficient on his or her platform. Some compilers treat char as unsigned (0 .. 255), and on others it’s signed (-128..127). Portable code should not rely on the signed-ness of char data types: use explicit unsigned char or signed char types if this matters.
Know Thy Language
At this point your head might be spinning — and we haven’t even covered the difference between “value preserving” and “unsigned preserving” short-to-int promotion rules. The rules are a hard area to cover well, and it brings home the point that there’s no substitute for really knowing your language well, though making friends with a local language guru is a good second choice.
It’s easiest to apply the techniques described here to new code — where the knowledge of the application is clear and fresh — and gradually let warning-free code creep into the rest of project. Retrofitting old code presents the biggest challenge: you need a firm understanding of the compiler and the intricacies of the application.
In either case, your efforts will pay off, as your project no longer resembles the work of the “sloppy developer” whose code you replaced.
Adding __attribute__ for format checking
Modern GNU compilers are able to inspect the arguments to printf()-like functions and compare the format string with the parameters provided. For example, passing a long parameter with a format string of %d would provoke a warning that in almost all cases point to a real problem.
What if you’d like to provide this kind of helpful checking to your own printf()-like functions? Use the GNU __attribute__ mechanism.
To illustrate, let’s create our own version of printf() that only functions if debugging is enabled. Here’s that code.
/* dprintf.c */
* Show how varargs can be validated
void dprintf(const char *format, …)
if ( Debug)
vfprintf(stderr, format, args);
Then, in the common library header file (“defs.h”), we’d include the prototype for the function:
extern void dprintf(const char *format, …)
__attribute__((format(printf, 1, 2)));
The format tag describes this as like printf(), that the format string is parameter 1, and the variable arguments begin at parameter 2. Then, using this function incorrectly in mainline code, as in…
const char *filename = “hello.txt”;
dprintf(“Opening file %d”, filename);
causes the compiler to notice that the parameter filename is a pointer, but that the format token is for an integer, %d. This is an outright bug that’s fixed by changing the format string to %s. Using the -W -Wall parameters to the compiler enables this checking.
More information on GNU attributes, includding other tags provided and how to use them portably, can be found at http://www.unixwiz.net/techtips/gnu-c-attributes.html.
Stephen Friedl is an independent consultant who has used UNIX for more than twenty years. Steve gets his email at steve@unixwiz. net. You can download the code used in this article from http://www.magazine.com/downloads/2003-08/compile. The signed- unsigned conversion tool can be found at http://www.unixwiz.net/usualconversions.cgi.