You have met the parent note and know the one-line rule: static_assert(condition, "message") makes the compiler refuse to build if condition evaluates to zero at compile time. This page does the opposite of theory — it grinds through every kind of case the tool can throw at you, so that when you hit one in the wild you have already seen its twin.
Before any example: the condition is a constant expression (see Constant expressions in C ). That means the compiler can compute a single fixed number for it — call that number v — without running the program . Then:
Definition Which standard, which spelling, which include?
In C11 / C17 the true keyword is ==_Static_assert==. The friendlier spelling static_assert is a macro defined in the <assert.h> header — so in C you must #include <assert.h> to write static_assert (or use the raw _Static_assert, which needs no include). See C11 vs C23 language features .
In C23 static_assert became a proper built-in keyword and the message became optional.
In C++ (since C++11, message optional since C++17) static_assert is a built-in keyword with no include needed — a common source of confusion, since C++ examples never #include <assert.h>.
Every C example on this page assumes #include <assert.h> at the top of the file unless it uses _Static_assert directly.
Think of static_assert as a machine that eats a constant v and outputs pass or fail . The "cases" are all the different shapes that constant can take. Here is every cell we will cover.
Cell
Case class
Representative question
A
Truthy value (any non-zero → pass)
sizeof(int) == 4 on a 32-bit box
B
Falsy value (exactly zero → fail)
same check on a 16-bit box
C
Degenerate: bare non-zero literal
static_assert(1) — does it even pass?
D
Zero-producing arithmetic
power-of-two mask x & (x-1)
E
Two things kept in sync
array length vs enum count
F
Boundary / limiting value
smallest legal size, x = 1, x = 0
G
Negative & signedness twist
-1 is truthy; unsigned wrap
H
Real-world word problem
packet header must fit a fixed wire size
I
Exam twist
which of these is not a constant expression?
Each example below is tagged with the cell(s) it covers. Together they touch every row.
Intuition Read this first, then guess each answer
Every example asks you to Forecast before you compute. Cover the steps, decide "pass or fail?", then check yourself. That is the fastest way to build the reflex.
Worked example Example 1 — what counts as "true"?
static_assert ( 1 , "never fails" );
static_assert ( 42 , "also never fails" );
static_assert ( sizeof ( char ), "char has non-zero size" );
Forecast: which of these three, if any, fails the build?
Look at the value v for each. Line 1: v = 1 . Line 2: v = 42 . Line 3: v = sizeof(char) = 1 (the C standard defines sizeof(char) as exactly 1 — see sizeof operator ).
Why this step? The rule cares only about "is v zero or not", so we first reduce each condition to its number.
Apply the rule. 1 = 0 , 42 = 0 , 1 = 0 — all non-zero.
Why this step? "Truthy" in C is any non-zero value, not just literally 1. This is the degenerate cell C: even a bare number is a legal condition.
Conclusion: all three pass and emit no code.
Verify: v ∈ { 1 , 42 , 1 } , none equal 0 , so none trigger a diagnostic. Runtime cost = 0 bytes.
Worked example Example 2 — a check that fails on purpose
#include <assert.h>
static_assert ( sizeof ( int ) == 4 , "This code assumes 32-bit int" );
Suppose we compile on an old micro-controller where int is 2 bytes .
Forecast: does this build succeed on that micro-controller?
Evaluate sizeof(int). On this platform it is 2 .
Why this step? sizeof is resolved by the compiler for the target machine, so its value is fixed at compile time — a valid constant.
Evaluate the comparison. 2 == 4 is false , and in C a false comparison produces the integer 0 . So v = 0 .
Why this step? Comparisons in C yield int values 1 (true) or 0 (false); we need that number, not the English word.
Apply the rule. v = 0 ⇒ build fails , printing "This code assumes 32-bit int".
Verify: On a 32-bit box the same line gives 4 == 4 ⇒ v = 1 ⇒ pass. Same source, opposite outcome — decided entirely by the target.
The figure below walks this exact machine. Read it left to right: the blue box is your condition; the yellow box is the compiler computing one fixed number v ; the diamond asks "is v == 0 ?". The green branch (taken when v = 0 , e.g. the 32-bit sizeof(int)==4 giving v = 1 ) leads to build OK, emits no code . The red branch (taken when v = 0 , e.g. the 16-bit case giving v = 0 ) leads to build fails, print message . That single diamond is the whole behaviour of static_assert.
The classic "must be a power of two" test uses the mask x & ( x − 1 ) , where & is bitwise AND. Here is why it works, in binary, before we assert with it.
x & ( x − 1 ) = 0 exactly for powers of two
A power of two has exactly one bit set: 8 = 100 0 2 . Subtracting 1 flips that single 1 to 0 and turns every bit below it into 1 : 7 = 011 1 2 . Line them up and AND them — there is no column where both are 1 , so the result is 0 . If x has two or more bits set, subtracting 1 leaves the higher bit untouched, so that column ANDs to 1 and the result is non-zero. Look at the two rows in the figure: the top block (x = 8 ) ANDs to all-zeros (green), the bottom block (x = 12 ) keeps a shared 1 -bit (red).
Worked example Example 3 — the power-of-two guard, boundary sweep
#define BUFFER_SIZE 1024 u /* unsigned constant */
static_assert ((BUFFER_SIZE & (BUFFER_SIZE - 1 )) == 0 ,
"BUFFER_SIZE must be a power of two" );
Forecast: pass or fail for BUFFER_SIZE = 1024? for 1000? for 1? for 0?
We write the literal as 1024u so BUFFER_SIZE is an unsigned constant. This matters for the degenerate case below.
x = 1024 = 2 10 . Then x − 1 = 1023 , and 1024 & 1023 = 0 . So the condition is 0 == 0 , i.e. v = 1 → pass .
Why this step? 1024 has one bit set (bit 10); the mask kills it. This is the intended good case.
x = 1000 (not a power of two). 1000 = 111110100 0 2 has several bits set. 1000 & 999 = 992 = 0 . Condition is 992 == 0 , i.e. v = 0 → fail .
Why this step? This is cell D: arithmetic that lands on zero-or-not. A non-power-of-two leaves a bit surviving, so the mask is non-zero and the assert catches it.
Boundary x = 1 = 2 0 . 1 & 0 = 0 → condition true → pass . 1 is a power of two.
Why this step? Cell F: the smallest legal value must not be a false negative. It passes correctly.
Degenerate x = 0 . Because BUFFER_SIZE is unsigned , 0u - 1 is well-defined : it wraps to the largest unsigned value (all bits 1 , e.g. 4294967295 for 32-bit unsigned). Then 0 & ( all ones ) = 0 → condition true → pass . But 0 is not a power of two — the trick has a blind spot at zero.
Why this step? Cell F again — the degenerate input. The type matters : for an unsigned BUFFER_SIZE, 0 - 1 wraps and is fully defined; for a signed one, 0 - 1 is simply − 1 (no underflow, since − 1 is representable). Either way 0 & anything == 0, so the mask alone wrongly accepts 0 . Add BUFFER_SIZE > 0 if 0 is possible.
Verify: 1024 & 1023 = 0 ; 1000 & 999 = 992 ; 1 & 0 = 0 ; 0 & ( 2 32 − 1 ) = 0 . Only the 1000 case fails the build — exactly the one we wanted to reject.
Common mistake The zero blind spot
(x & (x-1)) == 0 is true for x = 0 , yet 0 is not a power of two. If a config could be 0 , write x != 0 && (x & (x-1)) == 0. Forgetting this is the single most common power-of-two bug.
Worked example Example 4 — array length vs enum count
#include <assert.h>
enum { COLOR_COUNT = 3 };
const char * names [] = { "red" , "green" , "blue" };
static_assert ( sizeof (names) / sizeof ( names [ 0 ]) == COLOR_COUNT,
"names[] and COLOR_COUNT are out of sync" );
Forecast: a teammate adds "cyan" to names[] but leaves COLOR_COUNT = 3. Build or break?
Count the array at compile time. sizeof(names) is the total bytes of the array; sizeof(names[0]) is the bytes of one element. Their ratio is the element count — a constant, because array sizes are fixed at compile time.
Why this step? We need a compile-time number for the length; this ratio is the standard idiom (see sizeof operator ). With 4 strings the ratio is 4 .
Read the enum value. COLOR_COUNT = 3 — an enumeration constant , which is a constant expression.
Why this step? Both sides of == must be compile-time constants for static_assert to accept them.
Compare. 4 == 3 is false → v = 0 → build fails with the sync message.
Why this step? Cell E: two independent definitions that must agree. The assert turns a silent "off-by-one over the array" runtime bug into a loud compile error.
Verify: Before the edit, ratio = 3 , and 3 == 3 ⇒ v = 1 ⇒ pass. After the edit, ratio = 4 , and 4 == 3 ⇒ v = 0 ⇒ fail. The guard fires precisely when the two drift apart.
Worked example Example 5 — is
− 1 true?
static_assert ( - 1 , "does this pass?" );
static_assert (( unsigned ) 0 - 1 > 0 , "unsigned wrap" );
Forecast: Line 1 uses a negative number. Line 2 subtracts 1 from 0 . Which build, if any, fails?
Line 1: v = − 1 . Is − 1 zero? No. The rule is "non-zero → pass", and − 1 = 0 .
Why this step? Beginners expect only positive values to be "true", but truthiness in C is = 0 , so negatives are true too. Line 1 passes .
Line 2: evaluate (unsigned)0 - 1. In unsigned arithmetic there are no negatives; subtracting 1 from 0 wraps to the largest unsigned value (e.g. 4294967295 for 32-bit unsigned). That is a huge positive number.
Why this step? Cell G's twist: signed vs unsigned changes the value , and static_assert only sees the final value.
Compare. Is that huge number > 0 ? Yes. So the condition is true, v = 1 → pass .
Why this step? The wrap makes the comparison true — a genuine surprise if you reasoned "0 − 1 is negative, so > 0 is false". Signedness flips the conclusion.
Verify: − 1 = 0 (truthy). ( 2 32 − 1 ) > 0 is true. Both lines pass . Moral: reduce the condition to its actual integer value under C's rules before deciding.
Worked example Example 6 — a network packet header must fit the wire format
A protocol fixes each header at exactly 8 bytes . Your struct is:
#include <assert.h>
#include <stdint.h>
struct Header {
uint16_t type; // 2 bytes
uint16_t length; // 2 bytes
uint32_t checksum; // 4 bytes
};
static_assert ( sizeof ( struct Header) == 8 , "Header must be exactly 8 bytes on the wire" );
Forecast: 2 + 2 + 4 = 8 . Will this always pass?
Add the declared field sizes. uint16_t = 2, uint16_t = 2, uint32_t = 4. Naive sum = 8 .
Why this step? The wire format demands 8; we are checking the compiler agrees.
Account for padding. The compiler may insert padding between fields to align them. Here type and length are 2-byte aligned and pack tightly; checksum is 4-byte aligned and already starts at offset 4 — so no padding is needed and sizeof(struct Header) = 8 .
Why this step? Struct size is not always the sum of fields — padding can inflate it. Cell H's realism is exactly this: sizeof is the truth, not your arithmetic.
Apply the rule. 8 == 8 ⇒ v = 1 ⇒ pass .
Why this step? If someone later reorders fields or changes a type so padding appears (say, sizeof becomes 12 ), the assert fails, protecting the wire contract before deployment.
Verify: 2 + 2 + 4 = 8 with no forced padding on this layout ⇒ sizeof(struct Header) = 8 ⇒ 8 == 8 ⇒ pass. This is why static_assert on sizeof is the standard guard for on-wire structs.
Worked example Example 7 — spot the illegal condition
int n = get_count ();
static_assert (n == 4 , "compile-time?" ); // (a)
static_assert ( sizeof (n) == 4 , "size of n" ); // (b)
#define K 4
static_assert (K == 4 , "macro constant" ); // (c)
static_assert ( get_count () == 4 , "call it" ); // (d)
Forecast: which lines compile at all, and which are outright errors?
Line (a): n == 4. n is a runtime variable (its value comes from get_count()). Not a constant expression → hard error : the condition is not knowable at compile time.
Why this step? Cell I tests the boundary of "constant expression". Runtime data is forbidden — that is what runtime `assert` is for.
Line (b): sizeof(n) == 4. sizeof(n) asks the type's size, not n's value — resolved at compile time. Constant! For a 4-byte int, 4 == 4 ⇒ v = 1 ⇒ pass .
Why this step? Subtle twist: sizeof on a variable is legal in static_assert because only the type matters, not the runtime value.
Line (c): K == 4. K is a #define → the preprocessor replaces it with 4 → 4 == 4 is a constant → v = 1 ⇒ pass .
Why this step? #defined numbers and enum constants are the bread-and-butter of static_assert conditions.
Line (d): get_count() == 4. A function call — its result is only known by running the program. Not constant → hard error .
Why this step? Same lesson as (a): no runtime evaluation is allowed, ever.
Verify: Legal & passing: (b) and (c). Hard errors (not even a failed assertion — a malformed condition): (a) and (d). The dividing line is always: can the compiler compute one fixed number without running anything?
Recall Matrix cells vs examples (all nine confirmed)
A truthy value → Example 1 (1, 42, sizeof(char) all non-zero)
B falsy value → Example 2 (sizeof(int)==4 gives v = 0 on a 16-bit box)
C bare non-zero literal → Example 1 (static_assert(1) passes)
D zero-producing arithmetic → Example 3 (power-of-two mask)
E two things in sync → Example 4 (array length vs COLOR_COUNT)
F boundary / degenerate → Example 3 (the x = 1 and x = 0 cases)
G negative & signedness twist → Example 5 (-1 truthy; unsigned wrap)
H real-world word problem → Example 6 (8-byte packet header)
I exam twist → Example 7 (which condition is not a constant expression)
All nine cells A–I are covered — no scenario left unshown.
Mnemonic The universal reflex
"Reduce to a number, then ask: zero or not?" Pass = non-zero. Fail = exactly zero. Illegal = not a constant at all.
Does static_assert(-1) pass? Yes — − 1 = 0 is truthy in C.
Does (x & (x-1)) == 0 correctly reject x = 0? No — it is true for 0 , so add x != 0.
Is sizeof(n) == 4 a valid static_assert even if n is a variable? Yes — sizeof uses the type, resolved at compile time.
Why is get_count() == 4 illegal in static_assert? A function call is a runtime value, not a constant expression.
For 1000, what is 1000 & 999? 992 (non-zero), so the power-of-two assert fails.
Which header gives you the static_assert spelling in C11/C17? <assert.h> (or use the raw _Static_assert keyword with no include).