🚀 Modern C (C11 → C23)
static_assert: catch bugs before the program even exists
▶ Open the interactive lesson — free, no signupPlenty of crashes in shipped software are just assumptions nobody ever wrote down — "an int is 4 bytes here", "this table has an entry for every enum value". Today you learn the one-line habit that makes the compiler refuse to build your program until an assumption actually holds. A bug caught that way can never reach anyone's machine, because the broken binary is never created at all.
You already know assert() from debugging: it checks a condition while the program runs and aborts if it's false. But some bugs can be caught much, much earlier — before an executable is even produced. That's what C11's _Static_assert is for: an assertion the compiler checks, at compile time, for free.
▶ This spot has an interactive flow widget — open the interactive lesson to play with it.
Compile time vs run time
The difference is enormous. A failed assert() crashes in front of your users, on their machine, at 3 a.m. A failed static_assert refuses to compile on your machine — the broken binary never exists. The trade-off: a static assert can only check things the compiler can compute — integer constant expressions like sizeof, enum values, and arithmetic on literals. It cannot check user input, file contents, or anything known only at runtime.
#include <assert.h> /* gives you the static_assert macro (C11/C17) */
#include <stdio.h>
/* Write your assumptions down — at file scope, checked while parsing: */
static_assert(sizeof(int) == 4, "this code assumes 32-bit int");
static_assert(sizeof(void *) == 8, "this code assumes 64-bit pointers");
/* Classic ABI check: a network packet header must be EXACTLY 8 bytes */
struct packet {
unsigned char type;
unsigned char flags;
unsigned short len;
unsigned int seq;
};
static_assert(sizeof(struct packet) == 8, "packet layout broke!");
int main(void) {
printf("all compile-time checks passed before main() existed\n");
return 0;
}$ gcc -std=c17 check.c -o check && ./check all compile-time checks passed before main() existed # the asserts cost NOTHING at runtime — they left no trace in the binary
Notice where the asserts live: at file scope, outside any function. That's allowed — a static assert is a declaration, so it can appear at file scope, inside functions, and even inside a struct definition. Nothing runs; the compiler simply evaluates the condition while parsing.
🧠 Checkpoint: Which of these can static_assert check?
- That a file exists on disk
- That
sizeof(long) == 8 - That user input is positive
- That malloc succeeded
Show answer
That sizeof(long) == 8 — Only integer constant expressions — things the compiler can compute without running the program. Files, input, and malloc results exist only at runtime; those need assert() or real error handling.
Watching one fail
Here's the payoff. Suppose your code genuinely assumes long is 8 bytes (say, you pack pointers into longs). Add the assert, and on any platform where that's false — 32-bit Linux, or Windows where long is 4 bytes even in 64-bit builds — the build stops cold with your message:
$ gcc -std=c17 check.c -o check
check.c:6:1: error: static assertion failed: "this code assumes 64-bit pointers"
6 | static_assert(sizeof(void *) == 8, "this code assumes 64-bit pointers");
| ^~~~~~~~~~~~~
# build stops. No binary. The bug is caught at the earliest possible moment.Rule of thumb: every time you catch yourself assuming something about sizes, layout, or ranges ("an int is 4 bytes here", "this struct matches the wire format"), write the assumption down as a static_assert. Assumptions rot; asserts don't.
Three spellings, one feature
| standard | how you write it |
|---|---|
| C11 / C17 | _Static_assert(expr, "message") — the keyword; <assert.h> adds the nicer macro static_assert |
| C23 | static_assert is a real keyword (no header needed), and the message is optional: static_assert(sizeof(int) == 4); |
Why the ugly _Static_assert spelling first? Backwards compatibility: names starting with underscore + capital are reserved, so old code that happened to define its own static_assert couldn't break. C23 finally promoted the pretty name once the world had caught up. You'll see the same _Ugly → pretty pattern all through this part.
🧠 Checkpoint: What did C23 change about static asserts?
- They can now check runtime values
static_assertbecame a keyword and the message became optional- They were removed in favor of assert()
- They now abort at runtime instead
Show answer
static_assert became a keyword and the message became optional — C23 promoted static_assert from an assert.h macro to a true keyword, and made the second argument optional: static_assert(sizeof(int) == 4); is now legal.
Real-world use: keeping tables in sync
A classic maintenance bug: an enum grows, but a parallel array of names doesn't. Six months later someone indexes past the end. A static assert turns that silent landmine into an instant compile error:
#include <assert.h>
#include <stdio.h>
enum op { OP_ADD, OP_SUB, OP_MUL, OP_COUNT }; /* _COUNT sentinel trick */
static const char *op_names[] = { "add", "sub", "mul" };
/* If the enum and the table ever disagree, the BUILD breaks — not prod: */
static_assert(sizeof(op_names) / sizeof(op_names[0]) == OP_COUNT,
"op_names[] out of sync with enum op");
int main(void) {
for (int i = 0; i < OP_COUNT; i++)
printf("op %d = %s\n", i, op_names[i]);
return 0;
}Add OP_DIV to the enum and forget the string? The build fails with "op_names[] out of sync" — pointing you at the exact fix. This pattern (a _COUNT sentinel plus a static assert) is everywhere in production C.
▶ This spot has an interactive editor widget — open the interactive lesson to play with it.
🧠 Checkpoint: Why must static_assert messages help future readers, e.g. "packet must match wire format"?
- The message is printed every run
- The message becomes the compile error someone sees years later
- The linker requires unique messages
- Messages are mandatory in C23
Show answer
The message becomes the compile error someone sees years later — When the assert finally fires — often on a new platform, years later — your message IS the diagnostic. "Assertion failed: 1 == 2" helps nobody; a sentence explaining the assumption fixes the bug in minutes. (And C23 actually made messages optional, not mandatory.)
Next up: another thing the compiler knows at compile time — how your data must be aligned in memory, and how to query and control it.
▶ Practice this lesson interactively (with live gcc)