Skip to content
C++
Language
since C++26
Intermediate

Contracts (C++26)

C++26 contracts (P2900): [[pre:]], [[post:]], and contract_assert() — formal precondition and postcondition checking with configurable build modes and violation handlers.

Contracts (P2900)since C++26

Contracts attach preconditions ([[pre:]]), postconditions ([[post:]]), and mid-function assertions (contract_assert) directly to function signatures, making interface expectations machine-checkable, documentable, and separately controllable from production code.

Overview

Before C++26, C++ had no standard way to express interface contracts. Developers used:

  • assert() — disabled in release builds, no differentiation between internal state and public API contracts
  • Manual if + throw — verbose, pollutes logic, inconsistent
  • Comments — zero enforcement
  • [[nodiscard]] / GSL expects — non-standard conventions

Contracts (P2900, accepted for C++26) make preconditions, postconditions, and assertions first-class language features with:

  • Formal syntax attached to function declarations
  • Build-mode control: checked, unchecked, or observe-only (for logging without termination)
  • Tooling hooks: contract violation handlers, UB sanitizer integration
  • No overhead in unchecked mode — equivalent to assuming they hold (optimizer can use them)

Syntax

cpp
// Precondition: caller's responsibility
int divide(int a, int b)
    [[pre: b != 0]]
    [[pre: a >= 0]];

// Postcondition: function's promise
int absolute_value(int x)
    [[post r: r >= 0]];

// Both
double sqrt(double x)
    [[pre:  x >= 0.0]]
    [[post r: r >= 0.0 && r * r <= x * 1.001]];

// Mid-function assertion
void process(std::vector<int>& v) {
    sort(v.begin(), v.end());
    contract_assert(std::is_sorted(v.begin(), v.end()));
    // ... rest of processing
}

Preconditions [[pre:]]

A precondition is a boolean expression that callers must satisfy. Violation is undefined behavior (in unchecked mode) or a contract violation (in checked mode).

cpp
// Precondition on a class member
class BoundedBuffer {
    std::span<int> buf_;
    size_t size_ = 0;

public:
    void push(int value)
        [[pre: size_ < buf_.size()]]
    {
        buf_[size_++] = value;
    }

    int pop()
        [[pre: size_ > 0]]
        [[post r: size_ == std::prev(size_)]]  // imaginary; post sees 'this' changes
    {
        return buf_[--size_];
    }
};

Important: preconditions are checked on function entry, before the body executes. The expression is evaluated in the caller's context (sees the arguments, not locals).

Postconditions [[post]]

A postcondition declares what the function promises to the caller on normal return. The optional name after post binds the return value:

cpp
// Named return value binding
std::vector<int> sorted(std::vector<int> v)
    [[post result: std::is_sorted(result.begin(), result.end())]]
{
    std::sort(v.begin(), v.end());
    return v;
}

// No return value binding needed for void functions
void clear(std::vector<int>& v)
    [[post: v.empty()]]
{
    v.clear();
}

Postconditions are not checked if the function exits via exception — they only apply to normal return.

contract_assert

Use inside function bodies for mid-function invariant checks — replaces assert():

cpp
void binary_search_impl(std::span<const int> sorted, int target) {
    size_t lo = 0, hi = sorted.size();

    while (lo < hi) {
        size_t mid = lo + (hi - lo) / 2;
        contract_assert(mid < sorted.size());   // can't be out of range by construction

        if (sorted[mid] < target) lo = mid + 1;
        else hi = mid;
    }
    contract_assert(lo == hi);  // loop invariant satisfied
}

Unlike assert(), contract_assert participates in the same contract checking infrastructure — it can be checked, unchecked, or observe-only.

Build Modes

Contracts decouple the semantic level (precondition vs postcondition vs assertion) from the checking behavior:

ModeBehavior
checkedEvaluate the expression; on violation, call the violation handler (default: std::terminate)
uncheckedDo not evaluate; optimizer may assume conditions hold (strongest optimization)
observeEvaluate but only call a logging handler — never terminates

The build mode is implementation-defined (typically a compiler flag: -fcontracts=checked, etc.).

Violation Handlers

The default violation handler calls std::terminate. You can replace it:

cpp
#include <contracts>

void my_violation_handler(const std::contracts::contract_violation& v) {
    // v.location()   — source location
    // v.comment()    — the expression text
    // v.kind()       — pre/post/assert
    // v.detection_mode() — checked/observe
    std::cerr << "[CONTRACT] " << v.location().function_name()
              << ": " << v.comment() << "\n";
    // Don't terminate in observe mode:
    if (v.detection_mode() == std::contracts::detection_mode::checked) {
        std::terminate();
    }
}

Contracts vs assert() vs Exceptions

assert()ExceptionContracts
Disabled in releaseYes (NDEBUG)NoConfigurable
Expresses caller obligationsNoNoYes ([[pre:]])
Expresses return guaranteesNoNoYes ([[post:]])
Optimizer can useNoNoYes (unchecked mode)
Source location metadataLimitedManualBuilt-in
Separate from logicNoNoYes (on signature)

Contracts and noexcept

If a function is noexcept, contract violations bypass exception handling and call std::terminate directly. This preserves the noexcept guarantee even in checked mode.

cpp
void fast_path(int x) noexcept
    [[pre: x > 0]]
{
    // If x <= 0, contract violation → std::terminate
    // No exception is thrown
}

Interaction with Inheritance

Preconditions and postconditions on virtual functions follow the Liskov Substitution Principle:

  • A derived class override may only weaken preconditions (accept more inputs)
  • A derived class override may only strengthen postconditions (promise more)

The language enforces this: derived class preconditions are OR'd with base class preconditions, postconditions are AND'd.

Key Rules

  • Contracts on a function declaration apply to all declarations of that function — use consistent predicates
  • Precondition expressions must not have side effects (undefined behavior if they do)
  • Postcondition expressions see the state after the function body, before the return
  • contract_assert is the only contract form allowed inside a function body
  • In unchecked mode, the optimizer treats violated contracts as UB — profile before assuming inputs are always valid
  • Contracts are separate from types: you can't store a "contracted int" in a variable; contracts live on call sites

Compiler Support

P2900R14 was accepted for C++26, but as of mid-2026 implementations are still landing. The evaluation semantic names (ignore, observe, enforce, quick-enforce) and the -fcontract-semantic flag are used by both the GCC and Clang experimental branches.

CompilerStatusHow to enable
GCC 16Full P2900R14 support (experimental)-std=c++26 (or -std=gnu++26); semantics via -fcontract-semantic=enforce|observe|ignore
ClangExperimental branch by Eric Fiselier (Clang 22+)Not in mainline; build the contracts branch; same -fcontract-semantic flag
MSVCNot yet implementedTracked on VS Developer Community
EDG eccpNot yet listed—

Feature-test macro: __cpp_contracts >= 202502L

Note: GCC 16 ships -fcontract-semantic as the flag to select the evaluation semantic globally. Because the standard leaves the mechanism implementation-defined, confirm the exact flag against your compiler's release notes before shipping.

Build Mode Reference

The evaluation semantic is set at compile time and applies globally to all contract assertions in the translation unit. Use it to produce different build configurations from the same source.

cpp
// Same source, different compilation targets:
//   Development/test:  -fcontract-semantic=enforce
//   Staging/logging:   -fcontract-semantic=observe
//   Production:        -fcontract-semantic=ignore

double sqrt_safe(double x)
    [[pre: x >= 0.0]]
    [[post r: r >= 0.0]]
{
    return std::sqrt(x);
}
SemanticChecks predicateOn violationOn return from handler
ignoreNoNo effect; optimizer may treat violation as UB—
observeYesCalls handle_contract_violation, then continuesContinues normally
enforceYesCalls handle_contract_violation, then terminatesContract-terminates
quick-enforceYesTerminates immediately (no handler call)—

CMake integration — pass the flag through target_compile_options:

cmake
cmake_minimum_required(VERSION 3.25)
project(my_app)

add_executable(my_app main.cpp)
target_compile_options(my_app PRIVATE -std=c++26)

# Select build mode via a CMake cache variable:
set(CONTRACT_SEMANTIC "enforce" CACHE STRING
    "Contract evaluation semantic: ignore|observe|enforce|quick-enforce")

target_compile_options(my_app PRIVATE
    -fcontract-semantic=${CONTRACT_SEMANTIC})

Invoke with: cmake -DCONTRACT_SEMANTIC=observe .. for a logging build, or ignore for production.

ignore Mode and the Optimizer ([[assume]] Analogy)

In ignore mode the contract predicate is not evaluated and no check is emitted. However, because a violated precondition is undefined behavior by definition, the optimizer is allowed to assume the predicate holds — just as if you had written [[assume(predicate)]].

cpp
// In ignore mode, the compiler may assume b != 0 and
// remove the branch for division-by-zero protection entirely:
int divide(int a, int b)
    [[pre: b != 0]]
{
    return a / b;
}

// Equivalent optimizer hint without contracts:
int divide_assume(int a, int b) {
    [[assume(b != 0)]];   // C++23 — optimizer hint, no check
    return a / b;
}

The difference: contracts are part of the function's public interface (visible on the declaration), while [[assume]] is a local hint inside the body. In enforce mode contracts become checked; [[assume]] never checks.

Contracts in constexpr / consteval Functions

When a contract predicate is evaluated in a manifestly constant-evaluated context and the evaluation semantic is a terminating semantic (enforce or quick-enforce), a failing predicate makes the program ill-formed — it becomes a compile-time error. With observe, the compiler emits a diagnostic but continues.

cpp
constexpr int safe_index(int i, int size)
    [[pre: i >= 0 && i < size]]
{
    return i;
}

constexpr int ok  = safe_index(2, 10);  // OK
constexpr int bad = safe_index(-1, 10); // ill-formed in enforce mode:
                                        // contract violation at compile time

// consteval functions follow the same rule — violation is always
// a compile error because all calls are constant-evaluated:
consteval int must_be_positive(int x)
    [[pre: x > 0]]
{
    return x * 2;
}

consteval int r = must_be_positive(-5); // compile error

This gives contracts in consteval functions the strength of static_assert without requiring a separate check.

Contracts on Function Templates

Contracts work on function templates; the predicate is instantiated with the template arguments and can use concepts or type traits:

cpp
#include <concepts>
#include <type_traits>

// Predicate uses a concept expression:
template <std::integral T>
T clamp(T value, T lo, T hi)
    [[pre: lo <= hi]]
    [[post r: r >= lo && r <= hi]]
{
    if (value < lo) return lo;
    if (value > hi) return hi;
    return value;
}

// Predicate guards against unsupported types at runtime:
template <typename T>
T* checked_cast(void* ptr)
    [[pre: ptr != nullptr]]
    [[pre: std::is_pointer_v<T*>]]   // always true here; type-trait guards
{                                    // are more useful with SFINAE fallbacks
    return static_cast<T*>(ptr);
}

// Contracts on a lambda template (C++20 generic lambda):
auto dot_product = []<std::floating_point T>(
    std::span<const T> a, std::span<const T> b)
        [[pre: a.size() == b.size()]]
        -> T
{
    T sum = 0;
    for (std::size_t i = 0; i < a.size(); ++i) sum += a[i] * b[i];
    return sum;
};

Note: pre and post cannot be applied to defaulted (= default) or deleted (= delete) functions, nor to virtual functions (virtual function contracts are deferred to a later standard).

Testing Strategies for Contracts

observe mode is the ideal test-build semantic. It evaluates every predicate and calls your violation handler without terminating the process, so a test suite can catch every violated contract in a single run rather than aborting on the first one.

cpp
// C++26
// test_violation_handler.cpp — installed before running any tests
#include <contracts>
#include <vector>
#include <string>
#include <iostream>

struct Violation {
    std::string function;
    std::string comment;
    std::contracts::contract_kind kind;
};

static std::vector<Violation> g_violations;

void test_violation_handler(const std::contracts::contract_violation& v) {
    g_violations.push_back({
        std::string(v.location().function_name()),
        std::string(v.comment()),
        v.kind()
    });
    // Do NOT terminate — observe mode lets execution continue.
}

Build your test binary with -fcontract-semantic=observe and register the handler at startup. Then write test cases that deliberately pass bad arguments and assert that violations were recorded:

cpp
// C++26
// In your test framework (e.g. Catch2 / doctest):
TEST_CASE("divide precondition fires on zero denominator") {
    g_violations.clear();

    divide(10, 0);   // violates [[pre: b != 0]]

    REQUIRE(g_violations.size() == 1);
    CHECK(g_violations[0].kind == std::contracts::contract_kind::pre);
    CHECK(std::string(g_violations[0].comment).find("b != 0") != std::string::npos);
}

For production-path testing, compile a separate "contract-check" binary with enforce and run it against a fuzzer or property-based test suite; a terminated process signals a real contract breach that must be fixed before release.

Contracts and the Standard Library

The standard library does not yet carry [[pre:]] annotations. P2811 ("Contract Assertions on STL Functions") was proposed to retrofit preconditions onto functions like std::vector::operator[], std::span::subspan, and iterator invalidation rules, but it was not included in the C++26 ballot — it targets a future standard.

Until then, the practical situation is:

  • libc++ annotations — libc++ uses _LIBCPP_ASSERT macros internally, which are enabled by -D_LIBCPP_HARDENING_MODE=_LIBCPP_HARDENING_MODE_DEBUG. These are conceptually precondition checks but they are entirely separate from the P2900 contract infrastructure and do not call handle_contract_violation.
  • Wrapping standard containers — a pragmatic bridge is to write thin wrappers that forward to the standard type and express the contract explicitly:
cpp
// C++26
// Thin wrapper expressing std::vector::at semantics as a contract:
template <typename T>
T& checked_at(std::vector<T>& v, std::size_t i)
    [[pre: i < v.size()]]
{
    return v[i];   // UB-free: precondition guarantees validity
}

When P2811 or a successor is adopted, wrappers like this can be replaced with direct calls without changing call sites.

Migration from assert()

contract_assert is a drop-in replacement for assert() with a far richer contract model. The mechanical substitution is straightforward — assert(expr) becomes contract_assert(expr) — but the larger migration is in separating caller obligations ([[pre:]]) from internal invariants (contract_assert) and return guarantees ([[post:]]).

cpp
// C++26
// Before: classic assert() usage
#include <cassert>

int safe_divide(int a, int b) {
    assert(b != 0);         // internal guard — invisible to callers
    assert(a >= 0);
    int result = a / b;
    assert(result >= 0);    // post-condition buried in body
    return result;
}

// After: contracts express the same constraints on the interface
int safe_divide(int a, int b)
    [[pre:  b != 0]]
    [[pre:  a >= 0]]
    [[post r: r >= 0]]
{
    return a / b;           // body is clean; contracts document the interface
}

When adapting violation handlers to an existing logging framework, install a custom handle_contract_violation that forwards to your logger and only terminates for enforce-mode violations:

cpp
// C++26
#include <contracts>

// Assumes your project uses spdlog or similar:
void handle_contract_violation(const std::contracts::contract_violation& v) noexcept {
    log_critical("CONTRACT {} violated in {}: {}",
        v.kind() == std::contracts::contract_kind::pre  ? "PRE"  :
        v.kind() == std::contracts::contract_kind::post ? "POST" : "ASSERT",
        v.location().function_name(),
        v.comment());

    if (v.detection_mode() == std::contracts::detection_mode::enforce) {
        std::terminate();
    }
    // observe mode: log and continue
}

Gradual migration tip: leave existing assert() calls in place while adding contracts incrementally. Because NDEBUG and -fcontract-semantic are orthogonal flags, both mechanisms can coexist during a transition period without changing program behavior in any build configuration.