Skip to content
C++
Language
since C++11
Basic

static_assert and Compile-Time Checks

Compile-time assertions for type checking, configuration validation, template constraints, and platform assumptions — zero runtime overhead.

static_assert (C++11)since C++11

A compile-time assertion that fails with a compiler error if the condition is false, enforcing invariants about types, sizes, and template parameters with no runtime cost.

Overview

static_assert verifies program properties at compile time, catching bugs before code runs. Unlike runtime assertions, it leaves zero overhead in the final executable. Use it to document and enforce invariants about types, sizes, configurations, and platform assumptions.

The message parameter became optional in C++17:

cpp
static_assert(sizeof(int) >= 4);              // C++17: message optional
static_assert(sizeof(int) >= 4, "int too small");  // always valid

Syntax

cpp
// Basic form (C++11)
static_assert(condition, "diagnostic message");

// Optional message (C++17)
static_assert(condition);

// Can appear in any scope — file, class, function
void process() {
    static_assert(sizeof(int) == 4);
}

struct Packet {
    uint32_t id;
    static_assert(sizeof(Packet) == 4);
};

template<typename T>
class Buffer {
    static_assert(std::is_trivially_destructible_v<T>);  // C++17: _v variant
};

Examples

Type Checking with Type Traits

Type traits (introduced C++11) pair naturally with static_assert to constrain templates:

cpp
#include <type_traits>

template<typename T>
void serialize(const T& obj) {
    static_assert(std::is_trivially_copyable_v<T>,  // C++17: _v variant
        "serialize requires trivially copyable types (safe to memcpy)");
    std::byte buf[sizeof(T)];
    std::memcpy(buf, &obj, sizeof(T));
}

// Numeric type constraints
template<typename T>
T safe_add(T a, T b) {
    static_assert(std::is_integral_v<T>, "arithmetic only on integers");
    static_assert(!std::is_same_v<T, bool>, "use bit operators for bool");
    // overflow checking...
    return a + b;
}

// Size relationships for type conversions
template<typename From, typename To>
To narrow_cast(From v) {
    static_assert(sizeof(To) <= sizeof(From),
        "narrow_cast destination must be narrower than source");
    return static_cast<To>(v);
}

Configuration and Platform Checks

Enforce assumptions about the execution environment:

cpp
// Verify platform-specific guarantees
static_assert(CHAR_BIT == 8, "need 8-bit bytes");
static_assert(std::numeric_limits<float>::is_iec559,
    "need IEEE 754 floats for network serialization");

struct CacheConfig {
    static constexpr size_t CACHE_LINE = 64;
    
    static_assert(CACHE_LINE > 0 && (CACHE_LINE & (CACHE_LINE - 1)) == 0,
        "cache line size must be a power of 2");
};

Layout and ABI Verification

Guarantee struct layout for wire protocols, file formats, and binary interoperability:

cpp
struct WireFormat {
    uint32_t magic;
    uint16_t version;
    uint16_t flags;
    uint64_t timestamp;
};

// Verify exact size and field offsets
static_assert(sizeof(WireFormat) == 16);
static_assert(offsetof(WireFormat, version) == 4);
static_assert(offsetof(WireFormat, timestamp) == 8);

// Protect enum value invariants
enum class Status : uint8_t { Ok = 0, Error = 1, Timeout = 2 };
static_assert(static_cast<int>(Status::Timeout) == 2,
    "Status enum values changed — update wire protocol handlers");

Template Parameter Constraints

Validate non-type template parameters at instantiation:

cpp
template<size_t Bits>
class FixedInt {
    static_assert(Bits > 0 && Bits <= 64, "Bits must be in [1, 64]");
    static_assert((Bits & (Bits - 1)) == 0 || Bits == 1,
        "Bits must be a power of two");
    
    using Storage = std::conditional_t<
        (Bits <= 8),  uint8_t,
        std::conditional_t<(Bits <= 16), uint16_t,
        std::conditional_t<(Bits <= 32), uint32_t, uint64_t>>>;
    Storage value_;
};

// Ring buffer requires power-of-2 size for cheap modulo via bitmask
template<typename T, size_t N>
class RingBuffer {
    static_assert((N & (N - 1)) == 0, "size must be a power of 2");
    T buf_[N];
    size_t head_ = 0, tail_ = 0;
public:
    void push(T v) { buf_[head_++ & (N - 1)] = v; }
    T pop() { return buf_[tail_++ & (N - 1)]; }
};

RingBuffer<int, 16> ok;   // 2^4 — valid
// RingBuffer<int, 10> bad;  // compiler error: 10 is not power of 2

Concepts and Constraints (C++20)

Since C++20, prefer concept constraints in the signature for cleaner error messages:

cpp
#include <concepts>

// Concept directly in signature (preferred for user-facing templates)
template<std::integral T>
T gcd(T a, T b) { /* ... */ }

// static_assert for explicit, contextual messages
template<typename T>
class FastHashMap {
    static_assert(std::is_default_constructible_v<T>,
        "FastHashMap values must be default constructible");
    static_assert(std::equality_comparable<T>,  // C++20 concept
        "FastHashMap keys must be equality comparable");
};

// requires clause (C++20) — triggers before template body
template<typename T>
    requires std::is_trivially_copyable_v<T>
void fast_copy(T* dst, const T* src, size_t n) {
    std::memcpy(dst, src, n * sizeof(T));
}

Marking Unreachable Branches

Use static_assert(false) in if-constexpr chains to catch unhandled template cases:

cpp
template<typename T>
void process(T x) {
    if constexpr (std::is_integral_v<T>) {
        // handle integer
    } else if constexpr (std::is_floating_point_v<T>) {
        // handle float
    } else {
        static_assert(false, "process: unsupported type");  // caught at instantiation
    }
}

Best Practices

  1. Write diagnostic messages that explain context, not just the constraint:

    cpp
    // Better: explains why the check exists
    static_assert(std::is_integral_v<T>,
        "bit_count requires integral type for bitwise operations");
  2. Prefer concepts (C++20+) over static_assert + type traits for public template APIs — they generate clearer error messages at the call site and fail earlier in the compilation process.

  3. Check multiple related conditions together to catch compound requirements:

    cpp
    template<typename T>
    void serialize_bitpacked(const T& obj) {
        static_assert(std::is_trivially_copyable_v<T>);
        static_assert(sizeof(T) <= 256, "bitpacked format supports up to 256 bytes");
    }
  4. Document why invariants hold for layout assertions when serializing between systems:

    cpp
    // Explain the constraint origin (e.g., protocol version, hardware requirement)
    static_assert(offsetof(Header, version) == 4,
        "Header.version at offset 4 (network protocol requirement)");

Common Pitfalls

  • static_assert(false) outside of template code becomes an unconditional error. Always guard with if constexpr or a template condition that can be true or false depending on instantiation.

  • Forgetting that checks occur per instantiation, not at template definition. The same assertion is not re-checked for the same type:

    cpp
    template<typename T>
    void foo() { static_assert(sizeof(T) == 4); }
    
    foo<int>();    // checked
    foo<int>();    // same type — assertion cached, not re-checked
    foo<short>();  // checked again (different type)
  • Mixing static_assert and requires inconsistently. For public APIs, prefer requires in the signature for consistent, user-friendly error messages.

  • Confusing optional message syntax with C++17. Code using static_assert(cond) without a message will fail to compile on C++14 and earlier — clarify the minimum standard in your project documentation.


static_assert in Class Templates

Assertion fires at instantiation, not at the template definition. The compiler checks each static_assert inside a class template body once per unique set of template arguments, so a faulty specialization is caught only when you actually use it.

cpp
// C++17
#include <type_traits>

template<typename T>
class SmallVector {
    static_assert(std::is_integral_v<T>,
        "SmallVector<T>: T must be an integral type");
    static_assert(sizeof(T) <= 8,
        "SmallVector<T>: T too large for this container (max 8 bytes)");

    T storage_[16];
};

SmallVector<int>      ok;    // fine: int is integral, sizeof == 4
SmallVector<uint64_t> ok2;   // fine: uint64_t is integral, sizeof == 8
// SmallVector<double> bad;  // error: not integral
// SmallVector<std::string> bad2; // error: not integral, and sizeof > 8

You can also place a static_assert(false, "...") inside an explicit specialization to block dangerous or unsupported uses at the call site.

cpp
// C++17
template<typename T>
struct Codec {
    static_assert(sizeof(T) <= 64, "Codec<T>: payload exceeds 64-byte limit");
};

// Block void specializations with a clear message
template<>
struct Codec<void> {
    static_assert(sizeof(void*) == 0, // always false — intentional guard
        "Codec<void> is not allowed; use Codec<std::byte> for raw bytes");
};

static_assert with Concepts (C++20)

static_assert and concepts serve different roles even in C++20. Use a concept constraint in the function/class signature when you want the constraint to participate in overload resolution; use static_assert inside the body to document invariants that concepts cannot express, such as exact sizes or ABI requirements.

cpp
// C++20
#include <concepts>
#include <type_traits>

template<typename T>
    requires std::regular<T>
class Registry {
    // Concepts already guard the signature, but in-body asserts
    // document additional non-overload-resolution invariants:
    static_assert(std::is_nothrow_move_constructible_v<T>,
        "Registry<T>: move constructor must be noexcept for strong exception safety");
};

Some invariants simply cannot be expressed as concepts — for example, ABI-level size constraints on protocol headers that must be stable across compiler versions.

cpp
// C++20
#pragma pack(push, 1)
struct MyProtocol {
    uint32_t magic;
    uint32_t length;
    uint64_t checksum;
};
#pragma pack(pop)

// Concept cannot verify exact byte layout; static_assert can.
static_assert(sizeof(MyProtocol) == 16,
    "MyProtocol ABI broken: struct size changed, recheck padding and pack directives");
static_assert(alignof(MyProtocol) <= 8,
    "MyProtocol: alignment exceeds DMA buffer requirements");

static_assert in consteval Functions

consteval forces a function to execute at compile time, and static_assert inside it catches bad arguments before they become runtime bugs. Calling a consteval function with a non-constant expression is itself a compile error, so the guard is unconditional.

cpp
// C++20
consteval void validate_port(int port) {
    static_assert(port > 0 && port < 65536,
        "validate_port: port number must be in [1, 65535]");
}

// Good: literal constant, checked at compile time
consteval int make_port(int p) {
    static_assert(p > 0 && p < 65536, "port out of range");
    return p;
}

constexpr int HTTP  = make_port(80);    // OK
constexpr int HTTPS = make_port(443);   // OK
// constexpr int BAD = make_port(99999); // compile error

Unlike assert (stripped in release builds) or std::runtime_error (thrown at run time), a consteval guard with static_assert is always checked and produces a clear diagnostic at the usage site. This pattern is well-suited for compile-time validation of protocol constants, magic numbers, and port/address configuration.

cpp
// C++20
consteval uint32_t make_magic(uint8_t a, uint8_t b, uint8_t c, uint8_t d) {
    // Verify reserved range — no runtime overhead, no possibility of bypassing
    static_assert(a != 0, "magic byte[0] must not be zero (reserved)");
    return (uint32_t(a) << 24) | (uint32_t(b) << 16) | (uint32_t(c) << 8) | d;
}

Formatting Better Error Messages

Before C++26, static_assert messages are string literals — you cannot embed computed values directly. C++26 lifts this restriction and allows consteval-evaluated string expressions, including std::format, but no major compiler fully implements this yet.

cpp
// C++26 (not yet widely available)
// static_assert(sizeof(T) == 4,
//     std::format("Expected 4 bytes, got {}", sizeof(T)));

The standard pre-C++26 workaround is to encode type information in the diagnostic by triggering an error that mentions the type by name in the compiler's backtrace.

cpp
// C++17 — encode type in error output via std::type_identity
#include <type_traits>

template<typename T>
struct AssertTrivial {
    // When this fires, the compiler backtrace will show T's name
    static_assert(std::is_trivially_copyable_v<T>,
        "AssertTrivial<T>: T is not trivially copyable — see T in backtrace above");
    using type = T;
};

// Trigger deliberately to see the type in the error:
// AssertTrivial<std::string> bad; // error mentions std::string in context

You can also exploit std::type_identity as a documentation anchor: naming the template parameter in a helper alias forces the compiler to print the offending type alongside the assertion message.

cpp
// C++17
template<typename T, typename = std::type_identity<T>>
void require_pod(T) {
    static_assert(std::is_standard_layout_v<T> && std::is_trivial_v<T>,
        "require_pod: T (see template parameter above) must be a POD type");
}

static_assert for API Contract Enforcement

Placing static_assert at module or component boundaries makes implicit contracts explicit and turns silent misuse into compiler errors. This is especially useful in plugin systems, extensibility APIs, and cross-platform code with hard platform assumptions.

cpp
// C++11
#include <type_traits>

struct Plugin { virtual void run() = 0; virtual ~Plugin() = default; };

template<typename T>
class PluginRegistry {
    static_assert(std::is_base_of_v<Plugin, T>,
        "PluginRegistry<T>: T must derive from Plugin");
    static_assert(!std::is_abstract_v<T>,
        "PluginRegistry<T>: T must be a concrete (non-abstract) type");
};

Platform-level assumptions should be checked once at file scope so they fail loudly on any unsupported target rather than producing silent data corruption.

cpp
// C++11
// At the top of a platform-sensitive translation unit:
static_assert(sizeof(int)      == 4, "Platform assumption violated: sizeof(int) != 4");
static_assert(sizeof(void*)    == 8, "Platform assumption violated: expected 64-bit pointers");
static_assert(sizeof(long long) == 8, "Platform assumption violated: sizeof(long long) != 8");

In network protocol and serialization test code, static_assert on struct sizes acts as a regression guard: if a field is added or alignment changes, the build breaks before any packet is sent.

cpp
// C++11
// Network protocol struct — sizes must be stable for wire compatibility
struct PacketHeader   { uint32_t magic; uint16_t ver; uint16_t flags; };
struct PacketBody     { uint64_t ts; uint32_t seq; uint32_t len; };
struct PacketFooter   { uint32_t crc; };

static_assert(sizeof(PacketHeader) == 8,  "PacketHeader size changed — wire protocol broken");
static_assert(sizeof(PacketBody)   == 16, "PacketBody size changed — wire protocol broken");
static_assert(sizeof(PacketFooter) == 4,  "PacketFooter size changed — wire protocol broken");

Alternatives: Concepts vs static_assert vs requires

Choose the mechanism based on where you want the error to appear and whether the constraint should influence overload resolution.

ApproachSyntaxError atComposableSubsumption
static_assertbody of templateinstantiationnono
requires clausefunction/class signaturesubstitutionyes (&&, ||)yes (C++20)
named conceptconcept C = ...;substitutionyesyes

Use requires for function template constraints that participate in overload resolution — the compiler can pick a better overload or print a clean "no matching function" error. Use static_assert for in-body invariant checks (sizes, alignments, enum values) that are not part of the overload decision.

cpp
// C++20
#include <concepts>
#include <type_traits>

// 1. static_assert — checked inside body, does NOT affect overload resolution
template<typename T>
void process_v1(T x) {
    static_assert(std::integral<T>, "process_v1: T must be integral");
    // ...
}

// 2. requires clause — checked at substitution, participates in overloading
template<typename T>
    requires std::integral<T>
void process_v2(T x) { /* ... */ }

// 3. Named concept constraint — most reusable, supports subsumption
template<std::integral T>
void process_v3(T x) { /* ... */ }

When both process_v2<double> and process_v3<double> are called, the compiler reports "constraints not satisfied" at the call site with the concept name visible. process_v1<double> compiles the template body first and only then reports the static_assert failure, which typically produces a longer backtrace and occurs deeper in the error output.


See Also

  • Type Traits — std::is_trivially_copyable_v, std::is_integral_v (C++11/17) and std::equality_comparable (C++20) provide building blocks for constraints.
  • requires Clauses (C++20) — template constraints with cleaner syntax and earlier failure reporting.
  • if constexpr (C++17) — compile-time branching that often pairs with assertions on instantiated types.
  • Concepts (C++20) — named type constraints as alternatives to type-trait assertions in public APIs.
  • std::enable_if (C++11) — older SFINAE mechanism for enforcing constraints; superseded by concepts.