C / UNDEFINED BEHAVIOUR AND DEFENSIVE C
Reading warnings as errors with -Wall -Wextra -Werror
Build C with -Wall -Wextra -Werror, read each diagnostic as the bug it names, and answer it in the source instead of switching the flag off.
What you will learn
- Compile every file with -std=c11 -Wall -Wextra -Werror and keep the warning count at zero
- In C, -Wextra is what adds -Wsign-compare and -Wunused-parameter
- Answer a warning in the code with (void)x or an explicit cast, not by dropping the flag
- Demote one diagnostic with -Wno-error=name instead of turning -Werror off
Understanding Reading warnings as errors with -Wall -Wextra -Werror
The C standard only obliges a compiler to complain about syntax errors and constraint violations. Writing if (x = 0), comparing an int with a size_t, or ignoring a function parameter breaks no rule, so a conforming compiler may translate all of it in silence. Everything GCC or Clang tells you beyond that minimum is optional, and both gate it behind flags: -Wall is a curated set of checks the maintainers consider high value and low noise, while -Wextra is a second set that accepts a few more false positives for wider coverage. Neither one means all warnings, which is why the two are almost always written together.
Warnings that a build prints but tolerates turn into scrollback. Once a project emits forty of them nobody reads number forty-one, and number forty-one is the loop that overwrites an array. -Werror converts the warning count into a binary state you have to keep at zero, so each new diagnostic arrives alone, attached to the change that produced it, while the author still remembers the code. The price is that you now need a way to say I looked at this and it is intentional inside the source, because the flag is no longer negotiable.
The flags have limits worth knowing. Many diagnostics fall out of the same dataflow analysis the optimiser runs, so an -O0 build reports less than an -O2 build of identical source, and the compiler sees one translation unit at a time, so mismatches between files stay hidden unless prototypes in a shared header force them into view. Because the contents of -Wall and -Wextra are defined by the compiler version, code that was clean under one release can produce fresh diagnostics under the next, which is the argument for keeping -Wall -Wextra everywhere and -Werror only where you control which compiler runs.
<stdio.h>
int main(void)
{
int values[] = { 3, 8, 8, 2 };
int target = 8;
int matches = 0;
/* gcc -Wall here: "suggest parentheses around assignment used as
truth value [-Wparentheses]". With -Werror it is a build failure
instead of a wrong answer. */
for (int i = 0; i < 4; i++)
if (values[i] = target)
matches++;
printf("matches = %d of 4\n", matches);
printf("values = %d %d %d %d\n",
values[0], values[1], values[2], values[3]);
return 0;
}
Warnings are the compiler reporting bugs it already found while translating your code; -Wall -Wextra ask for the report and -Werror makes ignoring it impossible.
Worked examples
The warning only -Wextra gives you
A signed length compared against strlen is legal C, silent under -Wall, and wrong.
<stdio.h>
<string.h>
/* Returns -1 on failure, like plenty of real C APIs. */
static int read_len(void)
{
return -1;
}
int main(void)
{
const char *buf = "hi";
int len = read_len();
if (len < strlen(buf)) /* int against size_t */
puts("shorter");
else
puts("not shorter");
printf("len = %d, strlen = %d\n", len, (int)strlen(buf));
return 0;
}
Example explained
Line 1read_len returns -1, an ordinary int value right up to the moment it meets an unsigned operand.
Line 2strlen has type size_t, so the usual arithmetic conversions turn len into a huge unsigned value before < runs, and the test is false even though -1 is printed as smaller than 2 on the next line.
Line 3gcc -Wall says nothing about this in C; -Wextra enables -Wsign-compare, which reports a comparison of integer expressions of different signedness.
Line 4The fix is to check len < 0 first or to give it type size_t, not to cast strlen down to int.
Saying I meant that in the source
How a file stays clean under -Wall -Wextra -Werror without any warning being switched off.
<stdio.h>
/* The signature is fixed by a callback table; this handler
has no use for the payload. */
static int on_tick(long tick, void *payload)
{
(void)payload;
return (int)(tick % 2);
}
int main(void)
{
for (long t = 0; t < 4; t++)
printf("tick %ld -> %d\n", t, on_tick(t, NULL));
return 0;
}
Example explained
Line 1(void)payload; reads the parameter and throws the value away, which is the idiom that tells the compiler and the next reader that ignoring it is deliberate.
Line 2-Wunused-parameter comes from -Wextra, so deleting that one line keeps the file valid C but breaks the build under -Wextra -Werror.
Line 3The explicit (int) cast states the narrowing from long to int, which also keeps the line quiet if -Wconversion is ever added; -Wconversion is in neither -Wall nor -Wextra.
Line 4Nothing is suppressed globally here: every diagnostic is still enabled, there is simply nothing left for them to report.
Important notes
-Wall and -Wextra mean whatever the installed compiler version thinks is worth reporting, so keep -Werror in your own builds and CI and out of the flags shipped with released source, where a newer GCC can break somebody else's build with a warning that did not exist yet.
Under -Werror a warning inside a third-party header also stops your build; include such headers with -isystem instead of -I so the compiler treats them as system headers and stays quiet about code you cannot change.
Common mistakes
Reading the diagnostic and running the program anyway because the build succeeded: the -Wparentheses example above cheerfully prints matches = 4 of 4, and the wrong count surfaces later as bad data rather than as a compiler message.
Casting to make the message go away, as in if (len < (int)strlen(buf)): -Wsign-compare stops firing, the unchecked -1 is still accepted, and the cast now suggests to reviewers that somebody considered the signedness on purpose.
Assuming -Wall means all warnings and therefore never enabling -Wconversion, -Wshadow or -Wpedantic: silent narrowing conversions and shadowed locals stay invisible in a build that looks maximally strict.
Try it yourself
Change, predict, then run
Paste the main example into an online C compiler, add -Wall -Wextra -Werror to its flags box, and confirm the build now fails on the if line. Then change = to == and check that it prints matches = 2 of 4 with values still 3 8 8 2.
Open the C workspaceCheck your understanding
A file builds silently with gcc -std=c11 -Wall -Werror. A colleague adds -Wextra and the build now fails on if (len < strlen(buf)), where len is an int. What does that failure tell you?
- -Wextra changed how the comparison is evaluated, so the program behaves differently in the two builds
- -Wall already enables everything that matters, so this is a -Wextra false positive worth suppressing
- In C, -Wsign-compare belongs to -Wextra rather than -Wall; len is converted to size_t before the comparison, so a negative len becomes a huge value
- The failure comes from -Werror alone; with warnings left as warnings the comparison would be done in signed arithmetic
Show answer
Warning flags choose which diagnostics get printed and never change the meaning of the program, so the two answers that blame -Wextra or -Werror for a behaviour change are wrong: len was already being converted to size_t in the silent build. The tempting wrong answer is the false positive one, because -Wall is only a curated subset; in C it leaves -Wsign-compare to -Wextra, and -Wconversion and -Wshadow sit outside both.