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:
#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 |
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:
#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 |
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:
int f(int x) pre(x > 0) { return x; }
int main() { f(-1); }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 exceptionReplacing 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:
#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 benoexcept; that keeps them testable. - A violation during constant evaluation is a compile error ("contract predicate is false in constant expression"), just like UB.
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:
int count = 0;
int f(int x) pre(++count > 0) { return x; } // error: read-only locationSide 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:
int clamp_up(int x, int lo) post(r: r >= lo) { return x < lo ? lo : x; }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:
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
#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.
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
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:
#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));
}−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 reportspredicate_false(the exception is available viastd::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_violationmay throw — that's how tests observe violations. Don't make narrow-contract functionsnoexcept. - Predicates are const;
postneedsconstvalue parameters; no contracts on virtual functions (use NVI). - Contracts describe bugs, not expected failures, and the program must be correct with them ignored.