5.1.31 · D4C Programming

Exercises — Compile-time assertions — static_assert

2,304 words10 min readBack to topic

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. k is a runtime variable; even though its value is 8 on this line, the language only allows compile-time constants. Use plain assert for k.
  • (C) YES. All literals; the compiler computes 7 == 71.
  • (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 C23 static_assert is 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 == 80 → 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 always 1 by definition of the language. Value: 1 == 11.
  • (B) FAIL. The literal 0u is unsigned. In the comparison -1 is converted to unsigned, becoming a huge positive value (all bits set), so -1 < 0u is false → 0. This is the "usual arithmetic conversions" gotcha, and static_assert will refuse to build. Value computed: 0.
  • (C) PASS. 1 << 4 shifts the bit left 4 places = 16. 16 == 161.

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 == 4

Write 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 && 11 → 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:

  1. uses the real C23 keyword static_assert without including <assert.h> when compiled as C23, and
  2. falls back to _Static_assert under 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 ✔ (64 is 1000000₂, one set bit).
  • Holds header: 64 >= 8 → true ✔.
  • Tiles page: 4096 % 64 = 0 ✔ (4096 / 64 = 64).

All three are 1, so 1 && 1 && 11passes. Change RECORD_SIZE to 100 and every clause but one fails: 100 & 99 = 96 ≠ 0 → build stops.

Figure — Compile-time assertions — static_assert

Wrap-up recall

Numeric checks for the solutions above live in the verification block.