Intuition The ONE core idea
A static_assert is a promise you make about your code that the compiler checks and enforces before the program even exists . If the promise is false, the build stops — so you must first understand what "the compiler", "a constant", and "a value being zero or non-zero" actually mean.
Before you can read a single line of the parent note, you meet a pile of symbols and words: static_assert, sizeof, enum, #define, &, &&, "constant expression", "compile time", "diagnostic". This page builds each one from nothing , in the order that lets the next one stand on the previous.
Everything in this topic hinges on when something happens. So we start there.
Definition Two moments in a program's life
Compile time = the moment a tool called the compiler reads your text file and translates it into machine instructions. The program is not running yet — it does not even exist as a runnable thing.
Run time = later, when a person double-clicks the finished program and the machine actually executes those instructions.
Figure Figure 1 — the two clocks
On the left (cyan zone) the compiler turns your .c text into a program file. On the right (amber zone) that program runs. Follow the amber arrow: a static_assert is a gate on the left , checked before any program exists; the cyan assert note on the right only fires while running.
Intuition Why this split matters
A check on the left costs nothing when the program runs, and cannot be skipped , because if the check fails there is simply no program to run. A check on the right costs time every run and only fires if that exact line is reached.
Related runtime tool: assert (runtime assertions) lives on the right clock. static_assert is its left-clock cousin.
The rule for static_assert is: "if the value is zero, fail." So we must nail what a value is.
Definition Value, and truthiness in C
A value is just a number the compiler works out. In C there is no separate "true/false" type in the classic sense — instead:
The number 0 counts as false .
Any non-zero number (1, 2, −7, …) counts as true .
Picture a light switch encoded as a number: "off" is naturally 0 , "on" is "some current flowing" = not zero. C borrows this: nothing / empty / off = 0 = false; something present = true.
So when the parent says "if the condition is zero (false), compilation fails" — it means the compiler computes one number, and only the exact value 0 triggers the stop.
Before comparisons and sizeof, the most basic ingredient of any expression is a plain number written directly in the source. It has a name.
A literal is a value written directly in the text of the program , standing for itself — nothing has to be computed or looked up to know what it is. Examples:
integer literals : 4, 1024, 0 (and 0xFF = 255 written in hexadecimal, 010 = 8 written in octal),
character literal : 'A' (which is the integer 65 in disguise),
string literal : "message" (the text used for a static_assert's message).
Intuition Why literals matter here
A literal is the purest possible compile-time constant : the compiler reads 1024 and instantly knows the value, with no program running. Every constant expression is ultimately built by combining literals (and sizeofs, enums, #defines) with operators.
The condition inside static_assert usually looks like sizeof(int) == 4. That == is doing real work.
== operator
a == b is a question: "are these two numbers equal?" The whole expression itself becomes a number :
it becomes 1 (true) if they are equal,
it becomes 0 (false) if they are not.
4 == 4 ⟶ 1 2 == 4 ⟶ 0
= is not ==
A single = assigns (stores a value into a variable). A double == compares . Inside a static_assert you always want the comparison ==.
x = 4 means "put 4 into x"; x == 4 means "is x equal to 4?".
So static_assert(sizeof(int) == 4, "...") really means: compute the number produced by sizeof(int) == 4; if that number is 0, stop.
Definition The sizeof operator
sizeof(T) gives the number of bytes that a value of type T occupies in memory. A byte is the smallest addressable chunk of memory — think of it as one grid-square of storage.
Figure Figure 2 — sizeof counts bytes
Each type is a row of grid-squares (bytes). A char is one square. An int (on a typical machine) is four squares, so sizeof(int) is 4 . The compiler already knows this layout, so it hands you the count at compile time — no program has to run.
sizeof yields a size_t (an unsigned number)
The number sizeof produces is not a plain signed integer — its type is ==size_t==, an unsigned integer type (it can never be negative, since a size in bytes is never negative).
size_t with signed values
Because size_t is unsigned, comparing it against a negative signed number can surprise you: the negative value is first converted to a huge unsigned number. In a static_assert prefer comparing sizeof(...) against another non-negative constant (like 4, or another sizeof), which is exactly what the parent's examples do.
Intuition Why the topic needs
sizeof
The classic use of static_assert is "I assume int is 4 bytes." That assumption is a fact about the machine's layout, knowable before running . sizeof turns that layout fact into a number the compiler can compare. See sizeof operator for the full treatment.
sizeof is not always a compile-time constant
Since C99, C allows a variable-length array (VLA) whose length is a runtime value, e.g. int a[n];. For such an array sizeof(a) is computed at run time and is not a constant expression — so it cannot go inside a static_assert. The rule of thumb: sizeof on a type or on a fixed-size array is a compile-time constant; sizeof on a VLA is not.
sizeof measures how big a type is. Its close cousin _Alignof measures a different compile-time fact about a type, and it shows up in the constant-expression list, so we define it now.
_Alignof
A type's alignment is the byte spacing the machine insists its values sit on — an int (4 bytes) usually must start at an address that is a multiple of 4. _Alignof(T) gives that required spacing as a number (a size_t). Like sizeof, it is fixed by the machine's layout, so the compiler answers it at compile time .
Intuition Why it belongs here
Sometimes your assumption is not "how big is this type" but "is this type aligned strongly enough for my trick" — e.g. static_assert(_Alignof(int) >= 4, "need 4-byte alignment"). Because _Alignof(int) is a compile-time number, it is a legal ingredient of a static_assert condition, exactly like sizeof.
Definition The preprocessor and #define
Before the compiler proper runs, a first pass called the preprocessor does simple text substitution. #define BUFFER_SIZE 1024 tells it: "everywhere you later see the word BUFFER_SIZE, paste the text 1024."
So by the time the compiler sees your code, BUFFER_SIZE has already become 1024. That makes it a constant the compiler can use inside a static_assert.
#define is not a variable
A #defined name is not a box in memory that can change. It is pure copy-paste done before compilation. That is exactly why it is allowed in a compile-time check — nothing about it depends on running the program.
Common mistake Paste pitfalls: always parenthesise
Because #define is blind text substitution, #define HALF 512+512 inside HALF * 2 pastes as 512+512 * 2 = 1536, not 2048 — the missing parentheses changed the arithmetic. Also, a macro can be expanded many times in one expression, so an unparenthesised macro can silently break the constant expression you feed to static_assert. Fix: wrap macro bodies in parentheses: #define HALF (512+512).
An enum creates a group of named integer constants. enum { RED, GREEN, BLUE }; makes RED = 0, GREEN = 1, BLUE = 2 automatically (counting up from 0). You can also pin a custom start value , and later names auto-increment from there: enum { A = 4, B, C }; gives A = 4, B = 5, C = 6. A single pinned constant is common too: enum { COLOR_COUNT = 3 };.
Intuition Why the topic needs enums
Enum values are fixed at compile time , so they too are legal inside static_assert. The parent's "keep the array and COLOR_COUNT in sync" example works precisely because COLOR_COUNT is a compile-time constant, comparable against a compile-time array length. See Enumerations (enum) .
The parent's power-of-two check (x & (x-1)) == 0 uses &. That is not the logical "and" && — they answer different questions, so we define both.
&
Every integer is secretly a row of binary digits (bits , each 0 or 1). a & b lines the two rows up and keeps a 1 only where both have a 1 . It works bit-by-bit and produces a new number .
&&
a && b asks one yes/no question: "are both a and b true (non-zero)?" It produces just 1 or 0 — never a bit-pattern. 2 && 4 is 1 (both non-zero); 2 & 4 is 0 (they share no 1-bits). Both & and && are allowed inside a static_assert provided their operands are constant .
x & (x-1) == 0 detects a power of two
Amber squares are 1-bits. A power of two (here x = 8 ) has exactly one 1-bit. Subtracting 1 flips that bit off and turns every lower bit on (7 ). ANDing the two rows keeps a 1 only where both have one — nowhere — so the result is all-zero. That is the whole trick.
x, and can it be negative?
In the parent's example x is a compile-time constant — a #defined size like BUFFER_SIZE, not a runtime variable (a variable would be illegal here). The power-of-two trick assumes x is a non-negative value: sizes are never negative, so this is fine. On a signed, negative number the bit pattern (two's complement) makes x & (x-1) == 0 meaningless — another reason to only apply it to unsigned or clearly-positive constants like buffer sizes.
The constant-expression list also allows one more operator, ? :, which you may not have met. It is the only C operator with three parts.
Definition The conditional (ternary) operator
cond ? a : b reads as a question: "is cond true (non-zero)? If yes, the whole thing is a; if no, it is b." It picks one of two values based on a test, and the result is that value (unlike an if statement, which does not produce a value).
3 > 2 ? 10 : 20 ⟶ 10 1 == 2 ? 10 : 20 ⟶ 20
Intuition Why it belongs here
When every part (cond, a, b) is a compile-time constant, the compiler can pick the answer during translation, so the whole ? : expression is a constant — legal inside a static_assert. For instance static_assert((sizeof(int) == 4 ? 1 : 0), "...") is a (clumsy) way of writing the same size check.
Now every earlier piece assembles into the one requirement that governs static_assert.
Definition Constant expression
A constant expression is any expression whose value the compiler can compute fully on its own, without running the program . Legal ingredients — all defined above — include:
literals (4, 1024, 'A'),
sizeof on a type or fixed-size array (not a VLA),
_Alignof(T) — the alignment requirement of a type,
enum values and #defined constants,
operators like ==, &, &&, +, -, and the conditional cond ? a : b.
Forbidden: anything that needs the program to run — a variable read from input, a function call's result, or sizeof on a VLA.
Intuition The single sentence to remember
static_assert accepts only things on the compile-time clock . Every symbol above (literals, sizeof on fixed sizes, _Alignof, enum, #define, ==, &, &&, ? :) is compile-time — so they are all fair game. A variable set at runtime is not. Deep dive: Constant expressions in C .
Now that every ingredient exists, we can finally define the keyword this whole page serves.
static_assert construct
static_assert takes a constant expression and (in C11/C17) a message string , and does this: the compiler evaluates the expression; if it is non-zero the assertion vanishes (emits no code); if it is zero the compilation fails and the message is printed. You may place it wherever a declaration is allowed — at file scope, inside a struct, or inside a function body.
Definition What is a "diagnostic message"?
A diagnostic is the human-readable line the compiler prints when something is wrong — the same channel used for errors and warnings. The string you pass to static_assert is your words injected into that channel, so instead of a cryptic failure the compiler says exactly why: e.g.
error: static assertion failed: "BUFFER_SIZE must be a power of two".
Writing a clear message here is how you turn a build failure into a helpful instruction to the next programmer.
Definition The raw keyword
_Static_assert
In C11/C17 the actual language keyword is spelled _Static_assert (capital S, leading underscore — the reserved style C uses for new keywords so it never clashes with your own names). It is used exactly like the macro spelling:
_Static_assert(sizeof(int) == 4, "need 32-bit int");
Writing #include <assert.h> gives you the friendlier alias static_assert, which the header simply #defines to _Static_assert. So the two lines below are identical :
_Static_assert(E, "m"); static_assert(E, "m"); (after #include <assert.h> )
In C23 static_assert became a real keyword needing no header. See Header <assert.h> and C11 vs C23 language features .
Cover the right side and test yourself.
At which clock (compile or run) does static_assert act? Compile time — before the program exists.
In C, which single number counts as false? 0 (zero); everything non-zero is true.
What is a literal in C? A value written directly in the source that stands for itself, e.g. 4, 'A', "text".
What does the expression a == b evaluate to? 1 if equal, 0 if not — it becomes a number.
What does sizeof(T) return, and of what type? The number of bytes T occupies, as an unsigned size_t.
What does _Alignof(T) return? The required byte alignment (spacing) of type T, a compile-time size_t.
When is sizeof NOT a constant expression? When applied to a variable-length array (VLA) — its size is a runtime value.
How do you count a fixed array's elements at compile time? sizeof(arr) / sizeof(arr[0]).
Is a #defined name a runtime variable? No — it is text pasted in before compiling, so it is a constant.
What pitfall does an unparenthesised #define cause? Blind text substitution can change the arithmetic; always wrap the body in parentheses.
In enum { A = 4, B, C }, what are B and C? B = 5 and C = 6 — they auto-increment from the pinned start value.
Difference between & and &&? & combines numbers bit-by-bit; && asks a single yes/no "are both non-zero?".
What does cond ? a : b produce? a if cond is non-zero, otherwise b — it yields a value.
What does x & (x - 1) == 0 test, and what must x be? Whether x is a power of two; x must be a non-negative compile-time constant.
What is a diagnostic message in static_assert? The string the compiler prints on failure, explaining why the build stopped.
What is the raw C11 keyword behind static_assert, and how is it used? _Static_assert(expr, "msg") — identical usage; the macro static_assert in <assert.h> just aliases it.
Where may a static_assert be placed? Anywhere a declaration is allowed — file scope, inside a struct, or inside a function.