C++26 Contracts: pre, post and contract_assert, Evaluation Semantics and Runtime Cost
Key takeaways
C++26 Contracts put preconditions and postconditions into the function declaration and let the implementation and build configuration decide how they are checked. This guide follows the adopted P2900 design: the three syntaxes, the ignore/observe/enforce/quick_enforce semantics, the replaceable violation handler, how to work around the missing old() capture, and the rules that trip people up first.
Introduction
C++26 Contracts let you express a function’s preconditions and postconditions, plus assertions inside a function body, as language syntax. P2900 “Contracts for C++” was voted into the C++26 working draft at the February 2025 WG21 meeting, and everything in this guide follows that design.
What used to live in assert, comments, or hand-written checks now becomes part of the declaration, while whether a check actually runs and what happens when it fails is decided by the implementation and build configuration.
One warning before the examples: Contracts were added to C++20, pulled back out, and went through several redesigns afterwards. A lot of code online uses old(x), the [[expects: ...]] attribute syntax, “class invariant” declarations, or options like -fcontracts=enforce. None of those are part of adopted C++26 Contracts. This guide shows what you can actually write instead.
Prerequisites:
- C++ function basics
- Exceptions and debugging experience
What Are Contracts?
Design by Contract, and What C++26 Covers
Contracts come from Bertrand Meyer’s “Design by Contract”, which traditionally has three parts:
Function = Contract
- Precondition: what the caller must guarantee
- Postcondition: what the function guarantees on return
- Class invariant: what every object must always satisfy
C++26 supports only preconditions and postconditions as part of a function declaration, plus a contract_assert statement for checks inside a body. There is no class invariant syntax. To check an invariant, write a bool is_valid() const member and call it from post or contract_assert yourself.
Limitations of Existing Approaches
Using assert:
#include <cassert>
int divide(int a, int b) {
assert(b != 0); // disappears when NDEBUG is defined
return a / b;
}
- All-or-nothing via
NDEBUG, and when enabled it always callsabort() - Nothing tells you whether this is a precondition or an internal check
- The condition sits in the body, invisible to callers who only read the header
Manual validation:
int divide(int a, int b) {
if (b == 0) {
throw std::invalid_argument("Division by zero");
}
return a / b;
}
- Always pays the checking cost
- Caller bugs and recoverable runtime errors share the same exception path
C++26 Contracts:
int divide(int a, int b)
pre(b != 0) // precondition stated in the declaration
{
return a / b;
}
- The contract is visible in the declaration
- The evaluation semantic decides whether it is checked and what a failure does
- A replaceable violation handler gives one place to route diagnostics
Basic Syntax
pre and post are function contract specifiers. They go after the declarator (after the parameter list, const/noexcept, and any trailing return type) and before the function body or a constructor’s member initializer list. You can list several; they are evaluated in order. contract_assert is a statement used inside a function body.
Precondition: pre
#include <cmath>
int sqrt_int(int x)
pre(x >= 0) // x must not be negative
{
return static_cast<int>(std::sqrt(x));
}
// Multiple conditions
void process(int* ptr, int size)
pre(ptr != nullptr)
pre(size > 0)
{
for (int i = 0; i < size; i++) {
ptr[i] *= 2;
}
}
Postcondition: post
To refer to the return value, give it a name: post(name: condition).
#include <algorithm>
#include <vector>
int factorial(int n)
pre(n >= 0)
post(r: r > 0) // return value r must be positive
{
if (n == 0) return 1;
return n * factorial(n - 1);
}
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;
}
In-Body Check: contract_assert
contract_assert is the language-level replacement for assert. It is not part of the contract with callers; it checks a condition that must hold at one specific point in the body. You can use it inside a loop to check a condition that should hold on every iteration, but that is just a check at that point, not a class-invariant feature.
#include <algorithm>
#include <vector>
bool contains(const std::vector<int>& arr, int target)
pre(std::is_sorted(arr.begin(), arr.end()))
{
int left = 0, right = static_cast<int>(arr.size()) - 1;
while (left <= right) {
contract_assert(left >= 0 && right < static_cast<int>(arr.size()));
int mid = left + (right - left) / 2;
if (arr[mid] == target) {
return true;
} else if (arr[mid] < target) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return false;
}
Rules for Contract Predicates
Most first-time compile errors come from these rules:
- Implicit const-ification: inside a predicate, parameters, local variables, and members accessed through
thisare treated asconst. Calling a non-const member function or modifying anything fails to compile, so declare your checking helpersconst. - Value parameters used in
postmust beconst: if the body modified a parameter, a postcondition would check a different value from what the caller passed. So a non-reference parameter referenced in a postcondition must be declaredconst(on all declarations). Reference parameters are not subject to this rule. - No side effects: depending on the semantic a predicate may not be evaluated at all, and implementations are allowed to evaluate the same predicate more than once. Logging or counters inside a predicate make behavior depend on the build.
- An exception thrown from a predicate is itself treated as a contract violation.
The second rule changes how you declare functions whose postconditions mention parameters:
#include <cstddef>
#include <vector>
// ❌ start and end are not const, so post cannot refer to them
// std::vector<int> create_range(int start, int end) post(r: r.front() == start) ...
// ✅ declare the value parameters const
std::vector<int> create_range(const int start, const int end)
pre(start <= end)
post(r: r.size() == static_cast<std::size_t>(end - start + 1))
post(r: r.front() == start)
post(r: r.back() == end)
{
std::vector<int> v;
for (int i = start; i <= end; i++) {
v.push_back(i);
}
return v;
}
Evaluation Semantics and Violations
The Four Semantics
P2900 says each contract check is evaluated with one of four evaluation semantics. Which one is used is up to the implementation, typically selected with compiler options. Option names differ between compilers and some implementations are still experimental, so check your compiler’s documentation rather than copying flags from older articles.
| Semantic | Predicate evaluated | On violation |
|---|---|---|
ignore | No | Nothing |
observe | Yes | Call the violation handler; continue if it returns normally |
enforce | Yes | Call the violation handler, then terminate (how is implementation-defined) |
quick_enforce | Yes | Terminate immediately without calling the handler |
Common misunderstandings:
quick_enforcedoes not mean “check only simple conditions”. It evaluates every predicate; it just skips the handler and diagnostic construction on failure, which keeps code size and overhead down.- Violations are not thrown as exceptions. How
enforceends the program (std::terminate,std::abort, …) is implementation-defined. - Under
ignore, predicates still have to be valid code (names are looked up and types checked), and the compiler does not assume the predicate holds. Contracts are not[[assume]]; P2900 deliberately rules that out so that turning checks off does not turn a violation into amplified undefined behavior.
Behavior Comparison
#include <iostream>
int divide(int a, int b)
pre(b != 0)
{
return a / b;
}
int main() {
int result = divide(10, 0); // contract violation
std::cout << result << '\n';
}
| Semantic | What happens here |
|---|---|
| ignore | No check; a / b runs, and division by zero is undefined behavior as always |
| observe | Handler prints a diagnostic, execution continues and still reaches a / b |
| enforce | Handler is called, then the program terminates before a / b |
| quick_enforce | Immediate termination, no handler |
observe is useful when introducing contracts, but as this example shows, continuing after a violation can be dangerous when the following code depends on the condition.
The Violation Handler
On a detected violation (observe, enforce) the contract-violation handler is called with a std::contracts::contract_violation from <contracts>. It carries the source location (location(), a std::source_location), a textual description of the predicate (comment()), the kind of assertion (kind(): pre, post, or assert), the evaluation semantic, and more.
You can replace the default handler by defining a global handle_contract_violation. Whether replacement is supported is implementation-defined, so check your toolchain.
#include <contracts>
#include <cstdio>
// Replaces the default violation handler (where the implementation supports it)
void handle_contract_violation(const std::contracts::contract_violation& v) {
std::fprintf(stderr, "contract violation at %s:%u: %s\n",
v.location().file_name(),
static_cast<unsigned>(v.location().line()),
v.comment());
// To also get the default handler's output:
// std::contracts::invoke_default_contract_violation_handler(v);
}
Routing violations into your existing logging during an observe rollout gives you real data on which contracts actually fire before you move to enforce.
Compiler Support
As of September 2026, GCC has an experimental implementation based on P2900, commonly enabled with -std=c++26 -fcontracts. Work in Clang and MSVC is in progress. The option for choosing a semantic and support for handler replacement vary by implementation and version, so check the release notes. Many articles still describe GCC’s older experimental implementation based on the C++20 draft; its options and syntax do not match C++26.
Preconditions
Basic Usage
#include <cstddef>
#include <iostream>
#include <vector>
// Array index access
int get_element(const std::vector<int>& vec, std::size_t index)
pre(index < vec.size())
{
return vec[index];
}
// Pointer validation
void process_data(const int* data, std::size_t size)
pre(data != nullptr)
pre(size > 0)
{
for (std::size_t i = 0; i < size; i++) {
std::cout << data[i] << ' ';
}
}
// Range validation
double calculate_percentage(int part, int total)
pre(total > 0)
pre(part >= 0 && part <= total)
{
return (static_cast<double>(part) / total) * 100.0;
}
Splitting conditions into several pre specifiers instead of one big && makes the handler’s comment() and location point to the exact condition that failed.
Member Functions
Predicates on member functions can name members directly. Those members are treated as const, so only const member functions can be called. A constructor’s pre goes before the member initializer list.
class BankAccount {
private:
double balance;
public:
explicit BankAccount(double initial)
pre(initial >= 0)
: balance(initial) {}
void deposit(double amount)
pre(amount > 0)
post(balance > 0)
{
balance += amount;
}
void withdraw(double amount)
pre(amount > 0)
pre(amount <= balance)
post(balance >= 0)
{
balance -= amount;
}
double get_balance() const
post(r: r >= 0) // balance is never negative
{
return balance;
}
};
A postcondition like “the balance decreased by exactly amount” compares against the value before the call, and C++26 Contracts cannot express that directly. The next section shows the workaround.
Postconditions
Return Value Validation
#include <limits>
int abs_value(int x)
pre(x != std::numeric_limits<int>::min()) // -INT_MIN overflows
post(result: result >= 0)
{
return (x < 0) ? -x : x;
}
This precondition is the kind of thing you find by writing the postcondition first: asking which inputs make result >= 0 true exposes the INT_MIN overflow.
There Is No old()
Eiffel’s old, D’s out contracts, and earlier C++ proposals let a postcondition refer to a value as it was on entry. Adopted C++26 Contracts do not. Code like post(count == old(count) + 1) does not compile. Copy cost, non-copyable types and evaluation timing kept it out of the minimal design; it is a topic for follow-up proposals.
Two workarounds:
1. Copy the value into a local at the start and contract_assert before returning
class Counter {
private:
int count_ = 0;
public:
int count() const { return count_; }
void add(int n)
pre(n >= 0)
{
const int before = count_;
count_ += n;
contract_assert(count_ == before + n);
}
void reset()
post(count() == 0)
{
count_ = 0;
}
};
The copy into before happens regardless of the evaluation semantic. For an int the optimizer will remove an unused copy in an ignore build, but copying a large object may leave a real cost. Functions with several return paths need the check before each return.
2. Restructure so the postcondition refers only to the result and unmodified parameters
int incremented(const int value, const int n)
pre(n >= 0)
post(r: r == value + n)
{
return value + n;
}
The first works for state-changing members; the second fits where the computation can be split into a pure function.
A Parameter You Modify Cannot Appear in post
#include <algorithm>
#include <vector>
std::vector<int> sort_and_deduplicate(std::vector<int> v)
post(r: std::is_sorted(r.begin(), r.end()))
post(r: std::adjacent_find(r.begin(), r.end()) == r.end()) // no adjacent duplicates
{
std::sort(v.begin(), v.end());
v.erase(std::unique(v.begin(), v.end()), v.end());
return v;
}
It is tempting to add post(r: r.size() <= v.size()), but v is a value parameter the body modifies, so it cannot be const and cannot appear in a postcondition. Save the input size in a local and check it with contract_assert instead.
contract_assert
Checking a Condition at a Point in the Body
#include <algorithm>
#include <cstddef>
#include <vector>
void insertion_sort(std::vector<int>& v) {
for (std::size_t i = 1; i < v.size(); i++) {
int key = v[i];
int j = static_cast<int>(i) - 1;
while (j >= 0 && v[j] > key) {
v[j + 1] = v[j];
j--;
}
v[j + 1] = key;
contract_assert(std::is_sorted(v.begin(), v.begin() + i + 1)); // holds after each pass
}
}
Note that this check is O(i) per iteration, which adds O(N²) work in total and would turn an already-sorted input (normally O(N) for insertion sort) into an O(N²) run. Fine for tests, not something to leave on in a release build.
Differences from assert()
assert() | contract_assert | |
|---|---|---|
| Control | NDEBUG on/off | Evaluation semantic (ignore/observe/enforce/quick_enforce) |
| On failure | abort() | Violation handler and/or termination, depending on semantic |
| Kind | C library macro | C++26 language statement |
| Optimizer assumption | None | None (contracts are not assumptions) |
When to Use Which
pre: the caller’s responsibility, checked on entrypost: the function’s guarantee, checked on normal return (not when leaving via an exception)contract_assert: a condition that must hold at a specific point inside the body
Checking Class Invariants by Hand
With no invariant syntax, collect the invariant in a const member and call it from post on the constructor and on every member that changes state:
#include <cstddef>
#include <vector>
class CircularBuffer {
private:
std::vector<int> buffer;
std::size_t capacity;
std::size_t head = 0, tail = 0, count = 0;
bool is_valid() const {
return count <= capacity && head < capacity &&
tail < capacity && buffer.size() == capacity;
}
public:
explicit CircularBuffer(std::size_t cap)
pre(cap > 0)
post(is_valid())
: buffer(cap), capacity(cap) {}
void push(int value)
pre(!full())
post(!empty())
post(is_valid())
{
buffer[tail] = value;
tail = (tail + 1) % capacity;
count++;
}
int pop()
pre(!empty())
post(!full())
post(is_valid())
{
int value = buffer[head];
head = (head + 1) % capacity;
count--;
return value;
}
bool empty() const { return count == 0; }
bool full() const { return count == capacity; }
};
Concurrency Caveat
A pre is evaluated before the body takes a lock and a post after the body has released it, so predicates that read shared members are data races. Put shared-state checks in a contract_assert inside the locked region:
#include <mutex>
class ThreadSafeCounter {
mutable std::mutex mtx;
int count = 0;
public:
void increment() {
std::lock_guard<std::mutex> lock(mtx);
const int before = count;
++count;
contract_assert(count == before + 1); // shared state checked under the lock
}
};
Practical Adoption
Phased Introduction
Phase 1: Preconditions on public APIs
#include <cstddef>
void process_data(const char* data, std::size_t size)
pre(data != nullptr)
pre(size > 0)
{
// Implementation
}
Most asserts at the top of existing functions are candidates to become pre. The real gain of this step is having to decide, for each one, whether it is truly the caller’s responsibility or input validation that was done with assert for convenience.
Phase 2: Postconditions on important functions
#include <algorithm>
#include <iterator>
#include <vector>
std::vector<int> merge_sorted(const std::vector<int>& a,
const std::vector<int>& b)
pre(std::is_sorted(a.begin(), a.end()))
pre(std::is_sorted(b.begin(), b.end()))
post(r: std::is_sorted(r.begin(), r.end()))
post(r: r.size() == a.size() + b.size())
{
std::vector<int> out;
out.reserve(a.size() + b.size());
std::merge(a.begin(), a.end(), b.begin(), b.end(), std::back_inserter(out));
return out;
}
a and b are reference parameters, so they can appear in the postcondition without an extra const.
Phase 3: In-body checks in complex logic
void complex_algorithm() {
contract_assert(is_valid_state());
step1();
contract_assert(is_valid_state());
step2();
contract_assert(is_valid_state());
}
Choosing a Semantic per Build Configuration
Because option names are implementation-specific, here is the policy rather than flags:
| Build | Suggested semantic | Why |
|---|---|---|
| Debug / unit tests | enforce | Make violations fail loudly |
| Staging / canary | observe | See whether newly added contracts actually fire |
| Release | enforce or quick_enforce (ignore for hot paths, by team decision) | Avoid continuing in a broken state |
In CMake, put your compiler’s semantic option into per-configuration compile options (generator expressions such as $<CONFIG:Debug>).
Performance
The Cost Is the Predicate
The runtime cost of Contracts is essentially the cost of evaluating the predicate plus the violation path. pre(b != 0) is one branch; pre(std::is_sorted(...)) is O(N) on every call. A blanket “Contracts cost X%” figure is meaningless; it depends on which predicates you enable and with which semantic.
ignore: predicates are not evaluated, and there is no[[assume]]-style optimization benefit either.quick_enforce: predicates are evaluated, but the failure path skips the handler, so the generated code is smaller.observe/enforce: predicate cost plus the handler call on the failure path; the non-violating path mostly pays only for the predicate.
Keeping Costs Under Control
Keep expensive predicates out of hot function contracts. Checking an O(N) property on an O(log N) search defeats the reason for using binary search:
#include <algorithm>
#include <vector>
// ❌ Two O(N) checks per call
void process(const std::vector<int>& v)
pre(std::is_sorted(v.begin(), v.end()))
pre(std::all_of(v.begin(), v.end(), [](int x) { return x > 0; }))
{ /* ... */ }
// ✅ Cheap contract here; validate expensive properties once at the boundary or in tests
void process(const std::vector<int>& v)
pre(!v.empty())
{ /* ... */ }
C++26 has no syntax for assigning a semantic to an individual contract (older proposals had levels; attributes like [[likely_ignore]] do not exist). Semantics are chosen by the implementation, so in practice you vary them per translation unit or build configuration. Be careful mixing semantics for inline functions compiled in several translation units, since part of that behavior is left to the implementation.
Comparison with Existing Approaches
Contracts vs assert vs Manual Validation vs [[assume]]
| Approach | Active in release | Distinguishes pre/post | Used as optimizer assumption | Configurable failure behavior |
|---|---|---|---|---|
assert() | ❌ (stripped by NDEBUG) | ❌ | ❌ | ❌ (always abort()) |
Manual if/throw | ✅ | ❌ | ❌ | Caller can catch |
[[assume]] (C++23) | Not a check | ❌ | ✅ | N/A (undefined behavior if false) |
| C++26 Contracts | Depends on semantic | ✅ (pre/post/contract_assert) | ❌ | ✅ (four semantics + handler) |
Keep Exceptions for Expected Failures
Manual validation still belongs where the failure is expected and recoverable and API consumers need a specific exception type or error value. Contract violations cannot be caught. Use Contracts for conditions that mean “the program has a bug if this is false”.
#include <stdexcept>
class Account {
double balance = 0;
public:
// Expected, user-facing failure: keep an exception the caller can handle
void withdraw(double amount) {
if (amount > balance) {
throw std::invalid_argument("Insufficient balance");
}
balance -= amount;
}
// A caller bug, not a user-facing case: use a contract
void internal_transfer(double amount)
pre(amount > 0)
pre(amount <= balance)
{
balance -= amount;
}
};
If an expected condition is made a precondition, an ignore build drops the check and ordinary bad input can go straight into undefined behavior.
Troubleshooting
Problem 1: The Program Terminates on a Contract Violation
Under enforce, the handler prints a diagnostic and the program ends. The message format is implementation-specific, and since the violation is not an exception there is nothing to catch.
- Use the reported location and predicate text to find which contract failed.
- If the caller broke the contract, fix the call site:
if (b != 0) {
result = divide(a, b);
}
- If the contract was too strong (it rejected input that is actually expected), move the check out of the precondition and report it through a return value or exception.
- If you just added contracts to running code, running with
observefor a while shows how often they fire.
Don’t make a predicate “warn and continue” with something like pre(b != 0 || (std::cerr << "...", false)). Side effects in predicates may run zero times or several times depending on the semantic. Warnings belong in observe plus a violation handler.
Problem 2: Compile Errors Inside Predicates
- “Cannot call non-const member function on const object”: members are const inside predicates. Make checking helpers (
is_open(),size(), …)const. - Errors about a parameter referenced in
post: value parameters used in a postcondition must beconst. If the body must modify the parameter, save the entry value and usecontract_assert.
Problem 3: Examples Using old() or Invariant Syntax Don’t Compile
post(x == old(x) + 1), [[expects: ...]], [[ensures: ...]] and invariant declarations come from the C++20 draft or proposals that were not adopted. Rewrite them with the local-copy + contract_assert pattern and is_valid() const checks shown above.
Conclusion
C++26 Contracts put preconditions and postconditions into function declarations and standardize in-body checks with contract_assert. The adopted design is intentionally close to a minimum viable product: no old values, no class invariants, no per-contract semantics. In exchange it is explicit about what is standard and what is left to the implementation.
Summary:
- pre: what the caller must satisfy, visible in the declaration
- post: what the function guarantees on normal return; name the result with
post(r: ...) - contract_assert: a condition that must hold at a specific point in the body
- Semantics: ignore / observe / enforce / quick_enforce, chosen by implementation and build settings
- Violation handler:
handle_contract_violationfor consistent diagnostics (replaceability is implementation-defined) - Not included:
old(), class invariants, contracts as optimizer assumptions
Adoption strategy:
- Start with preconditions on public APIs (existing entry
asserts are candidates) - Add return-value postconditions to important functions
- Check state changes with a local copy +
contract_assert - Observe violations first, then switch to enforce
Next:
References:
Frequently Asked Questions (FAQ)
Q. Is it OK for a pre or post condition to call a function with side effects?
A. Avoid it. Depending on the semantic a predicate may not be evaluated at all (ignore), and the C++26 rules also allow an implementation to evaluate it more than once, so logging, counters or state changes inside a predicate make behavior depend on how the program was built. Keep predicates as cheap, pure checks. To help with this, parameters, local variables and members referenced in a contract predicate are treated as const, so accidental modification fails to compile.
Related Articles
- C++26 Reflection Basics: the ^^ Operator, std::meta::info and template for
- C++20 Concepts | Making Template Error Messages Readable
- C++26 Is Finalized: Reflection, Contracts, std::execution and Other Key Changes