Exercises — Compile-time assertions — static_assert
Before we start, one shared vocabulary reminder so no symbol is used unexplained:
Level 1 — Recognition
Exercise 1.1
For each snippet, answer yes/no: is the condition a constant expression that static_assert will accept?
static_assert(sizeof(long) >= 4, "a"); // (A)
int k = 8; static_assert(k == 8, "b"); // (B)
static_assert(3 + 4 == 7, "c"); // (C)
static_assert(getchar() != 0, "d"); // (D)Recall Solution
- (A) YES.
sizeof(long)is fixed by the compiler → constant. - (B) NO.
kis a runtime variable; even though its value is8on this line, the language only allows compile-time constants. Use plainassertfork. - (C) YES. All literals; the compiler computes
7 == 7→1. - (D) NO.
getchar()is a function call — it can only produce a value while the program runs.
Exercise 1.2
Which header must you #include to write the friendly spelling static_assert under C11? And what is the raw keyword if you include nothing?
Recall Solution
- Header:
<assert.h>(see Header <assert.h>). - Raw C11 keyword:
_Static_assert. In C23static_assertis itself a keyword and needs no header (see C11 vs C23 language features).
Level 2 — Application
Exercise 2.1
An algorithm packs 8 flags into one byte. Write a static_assert (C11-style, with message) that guarantees a char is exactly 8 bits by checking its byte count. (Use CHAR_BIT from <limits.h>.)
Recall Solution
#include <assert.h>
#include <limits.h>
static_assert(CHAR_BIT == 8, "This packing assumes 8-bit bytes");CHAR_BIT is a #defined constant → the whole condition is constant. On any (rare) machine where CHAR_BIT != 8, the build stops before the bit-packing bug can ship.
Exercise 2.2
You have a lookup table and a matching count. Fill in the static_assert so the two stay in sync:
enum { STATE_COUNT = 4 };
const char *state_name[] = { "idle", "run", "pause", "stop" };
static_assert( /* ??? */ , "state_name[] out of sync with STATE_COUNT");Recall Solution
static_assert(sizeof(state_name)/sizeof(state_name[0]) == STATE_COUNT,
"state_name[] out of sync with STATE_COUNT");sizeof(state_name) = total bytes of the array; sizeof(state_name[0]) = bytes of one element (a pointer). Their ratio is the number of elements, known at compile time. Currently that is 4 == STATE_COUNT → passes. Add a 5th name without bumping the enum and it becomes 5 == 4 → build fails. See sizeof operator and Enumerations (enum).
Exercise 2.3
Write a static_assert that a #defined ALIGNMENT of 16 is a positive power of two.
Recall Solution
#define ALIGNMENT 16
static_assert(ALIGNMENT > 0 && (ALIGNMENT & (ALIGNMENT - 1)) == 0,
"ALIGNMENT must be a positive power of two");The trick x & (x-1) clears the lowest set bit of x; for a power of two there is exactly one set bit, so the result is 0. For 16 (10000₂), 16 & 15 = 10000₂ & 01111₂ = 0 → passes. The extra ALIGNMENT > 0 guards the degenerate case 0, for which 0 & -1 == 0 would wrongly pass.
Level 3 — Analysis
Exercise 3.1
This code compiles on your laptop but a teammate reports a build error. Explain exactly which line fails and why, and how to make it portable.
#include <assert.h>
static_assert(sizeof(void*) == 8, "need 64-bit pointers");Recall Solution
Nothing is syntactically wrong. The failure is semantic: on a 32-bit target sizeof(void*) is 4, so 4 == 8 → 0 → the build stops with "need 64-bit pointers". That is static_assert doing its job — it caught a wrong assumption at build time on the teammate's machine. To make it portable, either handle both widths with #if/separate code paths, or relax the assertion to what the code truly needs (e.g. sizeof(void*) >= 4).
Exercise 3.2
Predict the result (pass / fail, and the numeric value the compiler computes) for each:
static_assert(sizeof(char) == 1, "P"); // (A)
static_assert(-1 < 0u, "Q"); // (B) tricky!
static_assert((1 << 4) == 16, "R"); // (C)Recall Solution
- (A) PASS.
sizeof(char)is always1by definition of the language. Value:1 == 1→1. - (B) FAIL. The literal
0uis unsigned. In the comparison-1is converted to unsigned, becoming a huge positive value (all bits set), so-1 < 0uis false →0. This is the "usual arithmetic conversions" gotcha, andstatic_assertwill refuse to build. Value computed:0. - (C) PASS.
1 << 4shifts the bit left 4 places =16.16 == 16→1.
Exercise 3.3
Why can #error not replace static_assert for the check sizeof(int) == 4?
Recall Solution
#error fires in the preprocessor, which runs before the compiler and only understands preprocessor tokens and #if arithmetic on macros/integer literals. It has no notion of types, so it cannot evaluate sizeof(int). static_assert runs one stage later, in the compiler, which does know type sizes. That is why size/enum checks belong in static_assert, not #error.
Level 4 — Synthesis
Exercise 4.1
Design a single static_assert that guarantees a struct has no padding by checking that its size equals the sum of its members' sizes:
struct Point { short x; short y; }; // want sizeof == 4Write the assertion, then state one platform where it could legitimately fail.
Recall Solution
static_assert(sizeof(struct Point) == sizeof(short) + sizeof(short),
"struct Point has unexpected padding");On a typical machine sizeof(short) == 2, so we assert 4 == 4 → passes. It could legitimately fail on a platform where the compiler inserts alignment padding (e.g. if short alignment forced a gap), making sizeof(struct Point) larger than 4. That is exactly the surprise you want caught at build time before doing raw byte I/O on the struct.
Exercise 4.2
Combine an enum and a sizeof check into one guard that fails if either a bit-mask enum grows past what fits in a uint8_t.
#include <stdint.h>
enum Perm { P_READ = 1, P_WRITE = 2, P_EXEC = 4, P_ALL = 7 };Write a static_assert ensuring P_ALL fits in one byte (value ≤ 255) and that a uint8_t is truly one byte.
Recall Solution
static_assert(P_ALL <= 255 && sizeof(uint8_t) == 1,
"Perm mask must fit in a single uint8_t");P_ALL is an enum constant (7) → constant expression; sizeof(uint8_t) is constant. Result: 7 <= 255 is 1, sizeof(uint8_t) == 1 is 1, 1 && 1 → 1 → passes. If someone later adds P_SPECIAL = 256, and sets P_ALL = 511, the first half becomes 0 and the build stops.
Level 5 — Mastery
Exercise 5.1
Write a portable header snippet that:
- uses the real C23 keyword
static_assertwithout including<assert.h>when compiled as C23, and - falls back to
_Static_assertunder C11/C17.
Use the __STDC_VERSION__ macro (201112L = C11, 202311L = C23).
Recall Solution
#if __STDC_VERSION__ >= 202311L
/* C23: static_assert is a keyword, message optional */
#define MY_SASSERT(cond, msg) static_assert(cond, msg)
#else
/* C11/C17: use the raw keyword so no header is required */
#define MY_SASSERT(cond, msg) _Static_assert(cond, msg)
#endif
MY_SASSERT(sizeof(int) >= 2, "int must hold at least 16 bits");Both spellings mean the same thing; we branch only to avoid depending on <assert.h> and to respect that C23 relaxed the message rule. See C11 vs C23 language features.
Exercise 5.2
A parser reads records of a fixed on-disk layout. You must guarantee at build time that:
RECORD_SIZE is a power of two, is at least sizeof(struct Header), and evenly tiles a PAGE of 4096 bytes. Given:
#define RECORD_SIZE 64
#define PAGE 4096
struct Header { int magic; int len; }; /* assume sizeof == 8 */Write one static_assert enforcing all three conditions and evaluate whether it passes.
Recall Solution
static_assert(
(RECORD_SIZE & (RECORD_SIZE - 1)) == 0 && /* power of two */
RECORD_SIZE >= sizeof(struct Header) && /* holds a header */
(PAGE % RECORD_SIZE) == 0, /* tiles the page */
"RECORD_SIZE constraint violated");Evaluate:
- Power of two:
64 & 63=0✔ (64is1000000₂, one set bit). - Holds header:
64 >= 8→ true ✔. - Tiles page:
4096 % 64=0✔ (4096 / 64 = 64).
All three are 1, so 1 && 1 && 1 → 1 → passes. Change RECORD_SIZE to 100 and every clause but one fails: 100 & 99 = 96 ≠ 0 → build stops.

Wrap-up recall
Numeric checks for the solutions above live in the verification block.