C / STANDARD LIBRARY TOUR
assert.h and failing loudly during development
Use assert() to state and enforce your code's own invariants, read a glibc assertion message, and keep debug and -DNDEBUG builds behaving identically.
What you will learn
- Guard preconditions with assert() so a violated one stops the program at that line
- Read a glibc failure line: program, file, line, function, stringified expression
- Keep assert conditions free of side effects so -DNDEBUG builds behave identically
- Check compile-time facts with static_assert, which NDEBUG cannot switch off
Understanding assert.h and failing loudly during development
An assert is a claim about your own program that you believe can never be false. `assert(lo <= hi)` says: whoever called this function has already guaranteed a sane interval. When the claim holds the macro does nothing at all; when it fails, glibc writes the program name, file, line, enclosing function and the source text of the condition to stderr and calls abort(). The value is in the timing — the program dies at the first violated assumption, while the arguments that broke it are still on the stack, instead of returning a plausible-looking wrong number that corrupts something twenty calls later.
Because assert is a macro rather than a function it can erase itself: if NDEBUG is defined before <assert.h> is included, the standard requires assert(expr) to expand to ((void)0), so the expression is never evaluated. Release builds therefore pay nothing, but any side effect written inside an assert — an increment, a read, an allocation — vanishes along with the check, and the two builds stop behaving alike. The line worth memorising is that assert checks your code while error handling checks the world: a failed malloc, a missing file, or nonsense typed by a user are things a correct program must expect, so they need an if and a return value, not an abort.
abort() is a deliberately rude exit. It raises SIGABRT, so atexit handlers do not run, whether buffered stdout is flushed is implementation-defined, and the shell reports `Aborted` with status 134 — in exchange you get a core dump or a debugger stopped in the exact failing frame, which is the whole payoff during development. For claims already fixed when the compiler runs — a struct's size, the width of an int, the number of rows in a table — assert.h also gives you static_assert, checked at translation time, free at runtime and untouched by NDEBUG. Asserts only earn their keep in builds people actually run, so leave them enabled in your test builds.
<assert.h>
<stdio.h>
/* clamp promises nothing if the caller hands it a reversed interval */
static int clamp(int v, int lo, int hi)
{
assert(lo <= hi);
if (v < lo) return lo;
if (v > hi) return hi;
return v;
}
int main(void)
{
printf("%d\n", clamp(7, 0, 10));
printf("%d\n", clamp(-4, 0, 10));
fflush(stdout); /* abort() may not flush for us */
printf("%d\n", clamp(5, 10, 0)); /* caller bug: lo > hi */
return 0;
}
An assert states a fact your own code guarantees, and its purpose is to kill the program at the exact line that guarantee broke rather than let corrupted state travel.
Worked examples
NDEBUG erases the condition
Shows that a disabled assert does not evaluate its expression, so side effects inside it disappear.
<stdio.h>
NDEBUG
<assert.h>
int main(void)
{
int calls = 0;
assert(++calls > 0); /* not evaluated at all in this build */
printf("calls = %d\n", calls);
assert(1 == 2); /* plainly false, and still nothing happens */
puts("still running");
return 0;
}
Example explained
Line 1`#define NDEBUG` must precede `#include <assert.h>`; the header inspects the macro every time it is included, so include order is what decides here.
Line 2`assert(++calls > 0)` expands to `((void)0)`, so `calls` is never incremented and printf reports 0.
Line 3`assert(1 == 2)` does not abort, which proves the condition was removed at preprocessing rather than tested and ignored.
Line 4Passing -DNDEBUG on the command line does the same for the whole translation unit, which is how release builds normally switch asserts off.
static_assert for facts the compiler already knows
Checks a struct layout at compile time, where a runtime assert would be both late and pointless.
<assert.h>
<stdio.h>
struct packet {
unsigned char kind;
unsigned char flags;
unsigned short length;
};
static_assert(sizeof(struct packet) == 4, "packet must stay 4 bytes on the wire");
int main(void)
{
struct packet p = { 3, 0, 128 };
printf("kind=%u length=%u size=%zu\n",
(unsigned)p.kind, (unsigned)p.length, sizeof p);
return 0;
}
Example explained
Line 1`static_assert` comes from <assert.h> in C11 and maps to the keyword _Static_assert; compile with `gcc -std=c11` or later.
Line 2It sits at file scope, outside any function, which a runtime assert cannot do because assert is a statement.
Line 3Change 4 to 8 and gcc stops with `static assertion failed: packet must stay 4 bytes on the wire` and produces no binary at all.
Line 4-DNDEBUG has no effect on it, so layout guarantees survive into release builds.
Attaching a message to a failing condition
Uses the && "string" idiom so the printed diagnostic explains the broken promise instead of only showing bit arithmetic.
<assert.h>
<stdio.h>
/* n must be a power of two */
static int shift_of(unsigned n)
{
assert(n != 0 && (n & (n - 1)) == 0 && "shift_of needs a power of two");
int s = 0;
while (n > 1u) { n >>= 1; s++; }
return s;
}
int main(void)
{
printf("%d %d\n", shift_of(1), shift_of(256));
fflush(stdout);
printf("%d\n", shift_of(48)); /* 48 is not a power of two */
return 0;
}
Example explained
Line 1The string literal cannot change the truth value because it decays to a non-null pointer, so its only effect is appearing in the message.
Line 2The macro stringifies the source text of the condition, which is why the diagnostic shows `(n & (n - 1)) == 0` and not the value 32 that actually failed.
Line 3`fflush(stdout)` makes sure `0 8` reaches the terminal first, since glibc's abort() does not flush streams.
Line 4`Aborted` is printed by the shell, not the program: it is how SIGABRT is reported, giving exit status 134.
Important notes
The wording of the failure message is implementation-defined — glibc prints `prog: file:line: function: Assertion `expr' failed.` and other libraries differ — so never grep for it from a test script.
A failed assert is abort(), not exit(): atexit handlers do not run and buffered stdout may be discarded, so flush or write to stderr anything you need to see next to the diagnostic.
Common mistakes
Hiding real work inside the condition, as in `assert(fgets(line, sizeof line, fp) != NULL)`: under -DNDEBUG the line is never read, so the shipped build silently skips input the tested build consumed.
Using assert for conditions the world controls, such as `assert(p = malloc(n))`: in release builds the assignment disappears and p is left unset, and in debug builds it kills the process instead of letting the caller report an allocation failure.
Writing `assert(n > 0, "n must be positive")`: in C17 and earlier assert takes exactly one argument and gcc rejects it with `macro "assert" passed 2 arguments, but takes just 1`; the form that works is `assert(n > 0 && "n must be positive")`.
Try it yourself
Change, predict, then run
Write `int average(const int *a, size_t n)` whose first line is `assert(a != NULL && n > 0)` and which returns the sum divided by n, then call it with a 3-element array and again with n = 0. Rebuild the same file with -DNDEBUG and compare the named assertion failure against the bare `Floating point exception` you get without the check.
Open the C workspaceCheck your understanding
A logging helper contains `assert(fputs(msg, log) >= 0);`. It works during development, but the release build, compiled with `-DNDEBUG -O2`, writes nothing to the log file. Why?
- NDEBUG makes assert return from the enclosing function as soon as it is reached, so the write never happens
- Optimization at -O2 deleted the fputs call because nothing uses its return value
- With NDEBUG defined, assert expands to ((void)0) and its argument is never evaluated, so fputs is never called
- NDEBUG only disables abort(), so the check still runs but the failure is ignored
Show answer
The standard specifies assert(expr) as ((void)0) when NDEBUG is defined, so the whole expression — including the function call — is gone at preprocessing time, before the compiler ever sees it. Option 1 is tempting because -O2 usually accompanies release builds, but a compiler may not delete a call that writes to a stream; that side effect is observable. NDEBUG also does not touch abort() itself, and assert never returns from the enclosing function.