Skip to content

Type Safety, Assertions & Contracts

Leverage the C++ type system to prevent bugs at compile time — strong enums, the newtype pattern, static_assert, runtime assertions, and the upcoming contracts feature.

Types as Documentation and Enforcement

The cheapest bug to fix is the one that never compiles. C++ has a powerful type system that can catch errors at compile time — but only if you use it deliberately. A function that takes two int parameters for width and height offers no protection against swapping them. Strong typing makes such mistakes impossible.

This lesson covers techniques to push error detection from runtime to compile time: enum classes for scoped enumerations, the newtype pattern for domain-specific types, static_assert for compile-time checks, assert for debug-time invariants, and a preview of C++26 contracts.

enum class: Scoped, Strongly-Typed Enumerations

Old-style enum values leak into the enclosing scope and implicitly convert to int, causing subtle bugs. enum class (C++11) fixes both issues: values are scoped and require explicit conversion.

enum_class.cpp
#include <iostream>
#include <cstdint>

// BAD: Old-style enum — pollutes scope, implicit int conversion
// enum Color { Red, Green, Blue };
// enum TrafficLight { Red, Yellow, Green };  // ERROR: Red, Green redefined!

// GOOD: enum class — scoped and type-safe
enum class Color : std::uint8_t {
    Red   = 0,
    Green = 1,
    Blue  = 2
};

enum class TrafficLight {
    Red,
    Yellow,
    Green   // No conflict with Color::Green
};

void set_pixel(Color c) {
    // Color values cannot accidentally be used as integers
    // int x = c;  // ERROR: no implicit conversion
    auto x = static_cast<std::uint8_t>(c);  // explicit conversion OK
    std::cout << "Color value: " << static_cast<int>(x) << '\n';
}

int main() {
    set_pixel(Color::Blue);
    // set_pixel(2);               // ERROR: int is not Color
    // set_pixel(TrafficLight::Red); // ERROR: wrong enum type

    // Switch with enum class — compiler warns about unhandled cases
    TrafficLight light = TrafficLight::Red;
    switch (light) {
        case TrafficLight::Red:    std::cout << "Stop\n";    break;
        case TrafficLight::Yellow: std::cout << "Caution\n"; break;
        case TrafficLight::Green:  std::cout << "Go\n";      break;
        // No default — compiler warns if a case is missing
    }
}

The Newtype Pattern: Preventing Argument Swap Bugs

The newtype pattern wraps a primitive type in a distinct type to prevent accidental misuse. This catches argument-swap bugs at compile time.

newtype.cpp
#include <iostream>
#include <compare>

// Problem: easy to swap width and height — compiles silently
// void resize(int width, int height);
// resize(height, width);  // Bug! Compiles fine.

// Solution: distinct types for Width and Height
struct Width {
    int value;
    explicit Width(int v) : value(v) {}
    auto operator<=>(const Width&) const = default;
};

struct Height {
    int value;
    explicit Height(int v) : value(v) {}
    auto operator<=>(const Height&) const = default;
};

void resize(Width w, Height h) {
    std::cout << "Resizing to " << w.value << "x" << h.value << '\n';
}

// For units that need arithmetic, use a tagged type template
template<typename Tag, typename T = double>
struct StrongType {
    T value;
    explicit StrongType(T v) : value(v) {}
    auto operator<=>(const StrongType&) const = default;
};

using Meters    = StrongType<struct MetersTag>;
using Seconds   = StrongType<struct SecondsTag>;
using Kilograms = StrongType<struct KilogramsTag>;

void apply_force(Kilograms mass, Meters distance) {
    std::cout << "Force applied: " << mass.value
              << " kg over " << distance.value << " m\n";
}

int main() {
    resize(Width{800}, Height{600});   // Correct — types enforce order
    // resize(Height{600}, Width{800}); // ERROR: type mismatch!

    apply_force(Kilograms{10.0}, Meters{5.0});
    // apply_force(Meters{5.0}, Kilograms{10.0}); // ERROR!
}

static_assert: Compile-Time Checks

static_assert verifies conditions at compile time. If the condition is false, compilation fails with your custom error message. Use it to enforce type properties, size constraints, and template preconditions.

static_assert_demo.cpp
#include <type_traits>
#include <cstdint>

// Ensure platform assumptions hold
static_assert(sizeof(int) >= 4,
    "This code requires int to be at least 32 bits");

static_assert(sizeof(void*) == 8,
    "This code requires a 64-bit platform");

// Enforce type constraints in templates
template<typename T>
class NumericBuffer {
    static_assert(std::is_arithmetic_v<T>,
        "NumericBuffer only works with numeric types");

    static_assert(!std::is_same_v<T, bool>,
        "NumericBuffer does not support bool (use bitset instead)");

public:
    void push(T value) { /* ... */ }
};

// Enforce struct layout for serialization
struct NetworkPacket {
    std::uint32_t source_ip;
    std::uint32_t dest_ip;
    std::uint16_t source_port;
    std::uint16_t dest_port;
    std::uint32_t sequence;
};

static_assert(sizeof(NetworkPacket) == 16,
    "NetworkPacket must be exactly 16 bytes for wire format");

int main() {
    NumericBuffer<double> buf;  // OK
    buf.push(3.14);

    // NumericBuffer<std::string> bad;  // ERROR: not arithmetic
    // NumericBuffer<bool> bad2;        // ERROR: bool not supported
}

Runtime Assertions and [[assume]]

assert(expr) (from ) checks a condition at runtime and calls std::abort() if it fails. Assertions are only active in debug builds — they are removed when NDEBUG is defined (i.e., in release builds).

Use assertions for programmer errors (invariant violations), not for user errors (bad input). An assertion failure means a bug in your code.

``cpp
#include
void process(int* data, int size) {
assert(data != nullptr && "data pointer must not be null");
assert(size > 0 && "size must be positive");
// ...
}
``

[[assume(expr)]] (C++23) tells the compiler that a condition is always true, enabling optimizations based on that assumption. Unlike assert, it has no runtime check — if the assumption is wrong, behavior is undefined.

``cpp
int divide_positive(int a, int b) {
[[assume(b > 0)]];
return a / b; // compiler can skip the check for b == 0
}
``

Use [[assume]] very sparingly and only when you have absolute certainty. It is an optimization hint, not a safety mechanism.

Note

C++26 introduces contracts: a language-level mechanism for preconditions, postconditions, and assertions. They replace ad-hoc uses of assert and [[assume]] with a standardized, configurable system.

``cpp
// C++26 contracts (preview syntax — may change)
int sqrt_int(int x)
pre(x >= 0) // precondition
post(r: r * r <= x) // postcondition (r is return value)
{
contract_assert(x < 1000000); // in-body assertion
// ...
}
``

Contracts can be configured at build time to check and abort, check and throw, assume (optimize), or ignore. This gives you the safety of assertions in debug mode and the performance of assumptions in release mode — without changing the source code.

Pitfall

Even with strong typing, these patterns undermine safety.

  • Using int for everything — booleans, indices, sizes, IDs, flags — loses all type checking
  • assert with side effects: assert(vec.push_back(x), true) — the push_back is removed in release builds!
  • Implicit conversions between numeric types: int x = 3.14; silently truncates — use static_cast explicitly or brace initialization int x{3.14}; which prevents narrowing
  • [[assume]] with false conditions is undefined behavior — it is strictly an optimization hint, not a check
Key Takeaways
  • enum class provides scoped, non-implicitly-converting enumerations — always prefer over old-style enum
  • The newtype pattern wraps primitives in distinct types, catching argument-swap bugs at compile time
  • static_assert enforces invariants at compile time with zero runtime cost
  • assert checks programmer invariants in debug builds — never use it for user input validation
  • [[assume]] (C++23) is an optimization hint, not a safety check — misuse causes undefined behavior
  • C++26 contracts will unify preconditions, postconditions, and assertions into a configurable language feature

Quiz — Test Your Knowledge

(15 XP)

1. Why is `enum class` preferred over old-style `enum` in modern C++?

2. What happens to `assert()` expressions in release builds (when `NDEBUG` is defined)?

3. What is the purpose of the newtype pattern?