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++11A 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:
static_assert(sizeof(int) >= 4); // C++17: message optional
static_assert(sizeof(int) >= 4, "int too small"); // always validSyntax
// 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:
#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:
// 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:
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:
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 2Concepts and Constraints (C++20)
Since C++20, prefer concept constraints in the signature for cleaner error messages:
#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:
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
-
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"); -
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. -
Check multiple related conditions together to catch compound requirements:
cpptemplate<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"); } -
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 withif constexpror 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:
cpptemplate<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_assertandrequiresinconsistently. For public APIs, preferrequiresin 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.
See Also
- Type Traits —
std::is_trivially_copyable_v,std::is_integral_v(C++11/17) andstd::equality_comparable(C++20) provide building blocks for constraints. requiresClauses (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.