JOVANA
Explore Library Glossary Getting Started Three Levels Fields How it works Mission
Join the mission
All guides

Compile-Time Safety: _Static_assert, alignas, restrict

Three small keywords that let you state a fact and have it checked or trusted before your program ever runs. _Static_assert turns an assumption into a build error, alignas controls where data lands in memory, and restrict hands the optimizer a promise it cannot otherwise make.

Moving a check from runtime back to build time

By now you know assert() from Vol I: it checks a condition while your program runs, and if the condition is false it aborts. That is useful, but it has two costs. The bad case is only caught if the line actually executes on some unlucky input, and the check itself spends a few instructions every time. This guide is about three modern-C tools that push work in the opposite direction — toward the compiler, before a single instruction runs. The first, _Static_assert, is the cleanest example: it is an assertion the language standard requires the compiler to evaluate at compile time, and if it is false, your build simply fails.

The shape is tiny: `_Static_assert(constant_expression, "message")`. The expression must be one the compiler can fully evaluate itself — a constant expression — typically built from sizeof, _Alignof, enum values, or arithmetic on literals. If it is non-zero (true), the line vanishes and produces no code whatsoever; it has zero runtime footprint. If it is zero (false), compilation stops with your message printed. In C23 you may also spell it `static_assert` without the underscore, and even drop the message in C23, but the underscored form with a message works everywhere from C11 onward.

Make it concrete. Suppose you have `struct packet_hdr` that you write straight to a socket, and your code relies on it being exactly 8 bytes with no padding. You can pin that down with `_Static_assert(sizeof(struct packet_hdr) == 8, "packet_hdr must be exactly 8 bytes")`, and guard a 32-bit-int assumption with `_Static_assert(sizeof(int) == 4, "assumes 32-bit int")`. Both expressions are pure sizeof arithmetic the compiler evaluates while building, so a false one is reported right there, not at runtime.

Read what just happened, because it is the whole point of _Static_assert. An assumption that used to live only in a comment is now a checked fact. If someone reorders the fields and the compiler inserts a padding byte, the build breaks on their machine, with a message naming the problem, instead of silently sending malformed packets in production. A runtime assert could only catch this if the right code path ran on the right input; the static one catches it for every build, every time, for free.

Where the data lands: alignas and alignof

The second tool needs one idea from Vol I made sharp. Every type has an alignment requirement: a number A (always a power of two) such that the object's address must be a multiple of A. A `uint32_t` typically has alignment 4, so it may only live at addresses like 0x1000 or 0x1004, never 0x1001. The CPU prefers this; on some architectures a misaligned access is slow, and on a few it traps outright. You already saw the consequence — padding inside structs exists precisely to keep each field on its required boundary. The new keyword _Alignof (C23 also lets you write `alignof`) simply asks a type its alignment as a constant expression: `_Alignof(uint32_t)` is 4 on a typical machine.

The companion _Alignas (C23: `alignas`) goes the other way: it requests an alignment for a variable or struct field, usually a stronger one than the type would get by itself. This is over-alignment, and it solves real problems. A buffer you feed to a SIMD instruction may need to start on a 16- or 32-byte boundary. A struct shared between threads benefits from sitting alone on a cache line — typically 64 bytes — so that two cores updating two different fields do not keep invalidating each other's cache. You spell the request right where you declare the thing.

In code it reads naturally. `alignas(64) unsigned char buf[1024]` declares a 1 KiB buffer guaranteed to start on a 64-byte boundary; writing `alignas(64)` on each of two hot `_Atomic` counter fields in a struct puts each on its own cache line so neighbouring updates cannot keep invalidating one another. And the two ideas compose beautifully: you can verify the request held with `_Static_assert(_Alignof(struct counters) == 64, "counters must be 64-byte aligned")`, turning an alignment you asked for into an alignment the build refuses to ship without. Checked and enforced, working together.

restrict: a promise the optimizer cannot make itself

The third tool is the subtlest, and it is a different kind of thing. _Static_assert and alignas are checked or enforced — get them wrong and the compiler tells you. restrict is the opposite: it is an unchecked promise you make to the compiler, much like the contracts behind undefined behavior. The problem it solves is aliasing — the possibility that two different pointers point at the same memory. When the compiler cannot rule that out, it must generate cautious, slower code, reloading values it would otherwise have kept in a register.

Picture the classic case. A function adds two arrays element by element into a third: `void add(int *dst, int *a, int *b, size_t n)`. Inside the loop the compiler writes to `dst[i]`. Could that write have changed `a` or `b`? If `dst` happens to overlap `a`, then yes — so on a strict reading the compiler must re-read `a[i]` and `b[i]` from memory on every iteration, because the previous store might have touched them. It cannot keep a base value in a register across the loop. That caution is correct but expensive, and it is invisible: the code looks fast, but the optimizer's hands are tied by a possibility you know will never happen.

/* Without restrict: dst may alias a or b, so the compiler must
   reload a[i] and b[i] every iteration -- it cannot prove safety. */
void add(int *dst, int *a, int *b, size_t n) {
    for (size_t i = 0; i < n; i++)
        dst[i] = a[i] + b[i];
}

/* With restrict: YOU promise dst, a, b never overlap. The compiler
   may now keep base pointers in registers, reorder, and vectorize. */
void add_r(int * restrict dst,
           int * restrict a,
           int * restrict b, size_t n) {
    for (size_t i = 0; i < n; i++)
        dst[i] = a[i] + b[i];
}

/* The promise is UNCHECKED. If you call add_r with overlapping
   arrays, the result is undefined behavior -- not a diagnostic. */
restrict is a pointer qualifier promising no aliasing; it unlocks optimizations but is never verified.

When you qualify a pointer with restrict, you are telling the compiler: *for the lifetime of this pointer, any object it reaches will be reached only through it (or pointers derived from it).* Given that promise, the compiler knows the store to `dst[i]` cannot have disturbed `a` or `b`, so it can hoist the array bases into registers, reorder loads and stores, and often auto-vectorize the loop into wide SIMD instructions. The speedup on tight numeric kernels can be large. This is why you see restrict throughout the C standard library — memcpy() is declared with it, which is part of why it is faster than the overlap-tolerant memmove().

How to use restrict without getting burned

Because the promise is unchecked, a broken restrict is a genuine landmine — and worse than most, because it follows the same rule as all UB. If you call `add_r` with overlapping arrays, the compiler does not warn you and does not clamp the result to something sensible; it generated code that assumed you told the truth, and the abstract machine has no defined behavior for the violation. The corruption may show only at `-O2`, only on some inputs, only after the optimizer chose to vectorize. Treat restrict exactly like the UB contracts from the last guide: a powerful tool that pays well when honored and punishes silently when broken.

  1. Reach for restrict only on hot loops where the profiler shows the cost — never sprinkle it everywhere. The vast majority of code is not bottlenecked by aliasing reloads, and the risk is not worth a guess.
  2. Before adding it, prove to yourself that the pointers can never overlap for any caller — across the whole pointer's lifetime, including aliasing through a pointer derived from another argument.
  3. Document the contract at the call site or in a comment, because the promise is invisible in the type to anyone reading a call. A future caller passing overlapping buffers is the classic way this bites.
  4. Verify the win and the correctness: compare the disassembly with and without restrict to confirm the optimizer actually vectorized, and run the tests under an address sanitizer to catch accidental overlap on real inputs.

It helps to keep restrict separate in your head from a neighbouring idea you will meet, strict aliasing. Strict aliasing is a rule the standard imposes on you by default: roughly, you may not access an object through a pointer of an incompatible type, and the compiler assumes you obey it. restrict is the reverse — a promise you choose to add about two pointers of the same type not overlapping. Both are about aliasing and both feed the optimizer, but one is a default obligation and the other is an opt-in claim. Confusing them is common; the clean line is type (strict aliasing) versus overlap (restrict).

One theme, three keywords

Step back and the three tools sit on a single spectrum: each lets you state a fact the compiler would otherwise have to assume the worst about, and each cashes that fact out earlier than runtime. _Static_assert states a fact and the compiler checks it for you, failing the build if you are wrong. alignas states a fact about placement and the compiler enforces it, refusing impossible requests. restrict states a fact about aliasing and the compiler trusts it, taking your word with no verification. Checked, enforced, trusted — three different deals with the same goal of pinning down what your program assumes.

Notice how this changes the feel of writing C. Assumptions that used to live only in your head — 'this struct is 8 bytes', 'this buffer is cache-aligned', 'these arrays never overlap' — can now be written into the code itself, where two of the three are verified mechanically. That is the modern-C move you have been seeing all through this rung, the same spirit as _Generic from the previous guide: take something the language used to leave implicit and dangerous, and give it a precise, compiler-visible spelling. The next guide turns to initialization — designated initializers, compound literals, and flexible array members — which lets you say what data is as clearly as these keywords let you say what you assume.