CodeRigor

FREE LESSON

Contracts (C++26)

State what callers must ensure and what a function promises with pre, post and contract_assert — and know exactly what happens when they're broken.

Every function has a contract: what the caller must make true before the call, and what the function promises in return. Until C++26 you wrote that contract in comments, enforced bits of it with assert, and hoped. C++26 lets you write it in the declaration, where tools, readers and the runtime can all see it:

C++
#include <cstddef>
#include <vector>

// Caller's job: index in range. Our promise: we return that element.
int element(const std::vector<int>& v, const std::size_t i)
    pre(i < v.size())
{
    return v[i];
}

int main() { return element({1, 2, 3}, 1) == 2 ? 0 : 1; }

This lesson uses GCC 16 (-fcontracts; CodeRigor adds the flag for you). Clang 23 doesn't implement contracts yet, so the editor's clangd will mark the syntax as an error — the GCC compile is the source of truth for this skill.

1. Three kinds of contract assertion

Syntax Where Checked
pre(expr) after the declarator, before the body on entry, after parameters are initialised
post(expr) or post(r: expr) same place on normal return; r names the return value
contract_assert(expr); a statement inside a body when control reaches it
Where each kind of assertion runs
Preconditions are the caller's obligations, checked on entry after the parameters are initialised. Postconditions are the function's promises, checked on a normal return (not when an exception leaves). contract_assert checks wherever control reaches it.
Where each kind of assertion runs
Preconditions are the caller's obligations, checked on entry after the parameters are initialised. Postconditions are the function's promises, checked on a normal return (not when an exception leaves). contract_assert checks wherever control reaches it.

They also work on constructors (a post there runs after the members are initialised), member functions and lambdas:

C++
#include <cstdio>

struct Fraction {
    long long num, den;
    Fraction(const long long n, const long long d)
        pre(d != 0)       // checked before the member initialisers run
        post(den > 0)     // checked after the constructor body
        : num(d < 0 ? -n : n), den(d < 0 ? -d : d) {}
};

int main() {
    auto square = [](const int x) pre(x >= 0) post(r: r >= x) { return x * x; };
    Fraction f(1, -2);
    std::printf("%lld/%lld %d\n", f.num, f.den, square(7));
}

There is no "old value" syntax: a postcondition can't say "size() is one more than it was". If you need that, check an intermediate state with contract_assert inside the body.

2. What happens when one is false

Each check runs with an evaluation semantic, chosen by the build (GCC: -fcontract-evaluation-semantic=..., default enforce):

Semantic Evaluates the predicate? On violation
ignore no nothing
observe yes calls the violation handler, then continues
enforce (GCC's default) yes calls the handler, then terminates
quick_enforce yes terminates immediately, no handler
A false predicate under each evaluation semantic
The build picks the semantic, not the code. Under enforce, a handler that throws turns termination into an exception — unless the function is noexcept, which terminates anyway (the Lakos rule).
A false predicate under each evaluation semantic
The build picks the semantic, not the code. Under enforce, a handler that throws turns termination into an exception — unless the function is noexcept, which terminates anyway (the Lakos rule).

The default handler prints what was violated. Built with plain -fcontracts:

C++
int f(int x) pre(x > 0) { return x; }
int main() { f(-1); }
Text
contract violation in function int f(int) at b.cpp:2: x > 0
[assertion_kind: pre, semantic: enforce, mode: predicate_false, terminating: yes]
terminate called without an active exception

Replacing the handler

A program may define one global function void handle_contract_violation(const std::contracts::contract_violation&). It receives the kind (pre/post/assert), the semantic, the predicate's text (comment()) and a source_location. The handler is allowed to throw: under enforce, an exception from the handler propagates out of the violated check instead of terminating. That is exactly how test frameworks — including the tests for this skill — observe violations:

C++
#include <contracts>
#include <cstdio>
#include <stdexcept>

void handle_contract_violation(const std::contracts::contract_violation& v) {
    std::printf("violated %s at line %u\n", v.comment(), v.location().line());
    throw std::logic_error("contract violated");
}

int halve(const int x) pre(x % 2 == 0) { return x / 2; }

int main() {
    std::printf("%d\n", halve(10));
    try {
        halve(7);
    } catch (const std::logic_error& e) {
        std::printf("caught: %s\n", e.what());
    }
}

Two consequences you must know:

  • The Lakos rule. If a function with a precondition is noexcept, an exception from the handler can't leave it, and the program terminates anyway. Functions with narrow contracts (ones with preconditions) should not be noexcept; that keeps them testable.
  • A violation during constant evaluation is a compile error ("contract predicate is false in constant expression"), just like UB.
C++
constexpr int positive(const int x) pre(x > 0) { return x; }
constexpr int y = positive(-1);

3. Rules the compiler enforces

Predicates see everything as const. Inside a contract, every named variable — parameters, locals, members via this, even globals in GCC — is treated as const, so you can't modify state by accident:

C++
int count = 0;
int f(int x) pre(++count > 0) { return x; }   // error: read-only location

Side effects through a function call still compile, and you must not rely on them: a predicate may be evaluated zero times (ignore) or more.

Value parameters used in a post must be declared const. The function could otherwise change the parameter before returning, and the postcondition would check the wrong value:

C++
int clamp_up(int x, int lo) post(r: r >= lo) { return x < lo ? lo : x; }
C++
int clamp_up(const int x, const int lo) post(r: r >= lo) { return x < lo ? lo : x; }

No contracts on virtual functions (GCC: "contracts cannot be added to virtual functions"). Put them on a non-virtual public function that calls a private virtual one — the non-virtual interface pattern:

C++
class Shape {
public:
    double area() const post(r: r >= 0) { return do_area(); }
    virtual ~Shape() = default;
private:
    virtual double do_area() const = 0;
};

Redeclarations must repeat the same contracts or none — GCC rejects a mismatched condition on a later declaration.

4. Writing good contracts

Wide vs narrow. A wide contract accepts every input (std::vector::at throws on a bad index). A narrow contract has preconditions (operator[]: calling it out of range is a bug in the caller). Contracts make narrow contracts explicit and checkable.

Contracts are for bugs, not for expected failures. A user typing an invalid date, a missing file, a network error — those are normal outcomes: return std::expected or throw. A precondition says "if this is false, the program is wrong". The test: would you be happy for the check to be compiled out (ignore) in production? If not, it isn't a contract.

Contracts are not control flow. The program must behave identically with every check ignored. Never rely on a predicate's side effects or on the handler to fix things up.

Good predicates are cheap and side-effect free. !empty(), i < size(), d != 0. An O(n) check like "the range is sorted" is still a legitimate precondition — you choose a cheaper semantic for release builds — but don't call functions that allocate, lock, or log.

Situation Use
Caller passed something the docs forbid pre
Function must guarantee something about its result post
An internal "this can't happen" state, mid-algorithm contract_assert
The input may legitimately be bad (user data, I/O) std::expected / exceptions
A check that must always run, even in release ordinary code (if + error path)

5. Worked example: a bounded stack

C++
#include <cstddef>
#include <cstdio>
#include <vector>

class BoundedStack {
public:
    explicit BoundedStack(const std::size_t capacity)
        pre(capacity > 0)
        : cap_(capacity) { data_.reserve(capacity); }

    // Narrow contracts, so deliberately not noexcept (Lakos rule).
    void push(const int v)
        pre(!full())
        post(!empty())
    { data_.push_back(v); }

    int pop()
        pre(!empty())
    {
        int v = data_.back();
        data_.pop_back();
        return v;
    }

    bool empty() const { return data_.empty(); }
    bool full() const { return data_.size() == cap_; }

private:
    std::size_t cap_;
    std::vector<int> data_;
};

int main() {
    BoundedStack s(2);
    s.push(1);
    s.push(2);
    std::printf("%d %d\n", s.pop(), s.pop());
}

Notice what isn't there: no if (full()) throw .... Pushing onto a full stack is the caller's bug; the precondition documents it, and the build's semantic decides whether it's checked.

Faded example — your turn

mean must not be called with an empty span, and its result lies between the smallest and largest element. Write the contract specifiers.

C++
double mean(std::span<const int> v)
    /* ??? */
{
    long long sum = 0;
    for (int x : v) sum += x;
    return static_cast<double>(sum) / static_cast<double>(v.size());
}
Answer
C++
double mean(const std::span<const int> v)
    pre(!v.empty())
    post(r: std::ranges::min(v) <= r && r <= std::ranges::max(v))

v must be const because the postcondition uses it.

6. Checking an algorithm's state with contract_assert

Some requirements can only be checked part-way through. An evaluator for a postfix program can't cheaply validate the whole program up front, but each step knows what it needs:

C++
#include <cstdio>
#include <span>
#include <vector>

// Non-negative numbers push themselves; -1 means "add the top two".
int eval(std::span<const int> program) {
    std::vector<int> stack;
    for (int op : program) {
        if (op >= 0) {
            stack.push_back(op);
            continue;
        }
        contract_assert(stack.size() >= 2);
        int b = stack.back();
        stack.pop_back();
        stack.back() += b;
    }
    contract_assert(stack.size() == 1);
    return stack.back();
}

int main() {
    int program[] = {2, 3, -1, 4, -1};
    std::printf("%d\n", eval(program));
}
eval({2, 3, −1, 4, −1}): the stack, and the checks at each −1
start / 5
Each −1 needs two operands, so contract_assert(stack.size() >= 2) sits right before the pop. At the end, contract_assert(stack.size() == 1) checks the whole program was well formed.
eval({2, 3, −1, 4, −1}): the stack, and the checks at each −1
Each −1 needs two operands, so contract_assert(stack.size() >= 2) sits right before the pop. At the end, contract_assert(stack.size() == 1) checks the whole program was well formed.

7. GCC 16 vs the standard (P2900)

  • When a predicate throws, P2900 reports detection_mode::evaluation_exception; GCC 16 reports predicate_false (the exception is available via std::current_exception() inside the handler).
  • GCC const-ifies globals referenced in predicates too.
  • Clang 23 has no contracts support yet.

Summary

  • pre (caller's obligations), post(r: ...) (your promises), contract_assert (internal states).
  • Violations call the handler; the semantic (ignore/observe/enforce/quick_enforce) decides what follows. GCC defaults to enforce.
  • A replaceable handle_contract_violation may throw — that's how tests observe violations. Don't make narrow-contract functions noexcept.
  • Predicates are const; post needs const value parameters; no contracts on virtual functions (use NVI).
  • Contracts describe bugs, not expected failures, and the program must be correct with them ignored.

Watch & read

Free lectures, talks and readings that cover this skill from another angle. All resources →

C++26 contracts: myths, misconceptions and how pre/post/contract_assert fit defensive programming.

The P2900 design from its co-author: syntax, evaluation semantics and the violation handler.

A conceptual view of contracts as the boundary between caller and callee responsibilities.

Quick hands-on intro to C++26 contracts.