C++26 Contracts Complete Guide
이 글의 핵심
Express function preconditions, postconditions, and invariants at language level with C++26 Contracts. From pre, post, contract_assert syntax to build modes and practical patterns.
Introduction
C++26’s Contracts is a feature that expresses function preconditions, postconditions, and invariants at language level. What was previously handled with assert, comments, or manual validation code can now be specified with standard syntax and controlled with compiler flags.
This guide explains Contracts basic syntax (pre, post, contract_assert), build modes, practical patterns, and comparison with existing approaches with code examples.
Prerequisites:
- C++ function basics
- Exception handling understanding (Exception Guide)
- Debugging experience
Reality in Production
When learning development, everything seems clean and theoretical. But production is different. Wrestling with legacy code, chasing tight deadlines, facing unexpected bugs. The content covered here was initially learned as theory, but through applying it to real projects, I realized “ah, that’s why it’s designed this way.” Particularly memorable are the trials and errors from my first project. I followed what I learned from books but couldn’t figure out why it didn’t work, spending days stuck. Eventually discovered the problem through senior developer code review, learning a lot in the process. This guide covers not just theory but also pitfalls and solutions you might encounter in practice.
Table of Contents
- What is Contracts?
- Basic Syntax
- Build Modes
- Precondition
- Postcondition
- Assertion
- Practical Patterns
- Performance Impact
- Comparison with Existing Approaches
- Troubleshooting
- Conclusion
What is Contracts?
Design by Contract
Contracts introduces Bertrand Meyer’s “Design by Contract” concept to C++:
Function = Contract
- Precondition: What caller must guarantee
- Postcondition: What function will guarantee
- Invariant: What must always be true
Limitations of Existing Approaches
Using assert:
#include <cassert>
int divide(int a, int b) {
assert(b != 0); // Only works in debug builds
return a / b;
}
- Ignored in Release builds
- Unclear if precondition or invariant
- No hints for compiler optimization Manual validation:
int divide(int a, int b) {
if (b == 0) {
throw std::invalid_argument("Division by zero");
}
return a / b;
}
- Performance overhead (always checks)
- Increased exception handling complexity
- Contract intent not clear C++26 Contracts:
int divide(int a, int b)
pre(b != 0) // Explicit precondition
{
return a / b;
}
- Clear intent
- Control validation level with build mode
- Enables compiler optimization
Basic Syntax
Precondition: pre
// Specify precondition after function declaration
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
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);
}
// Named return value
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;
}
Invariant: contract_assert
void binary_search(const std::vector<int>& arr, int target)
pre(std::is_sorted(arr.begin(), arr.end()))
{
int left = 0, right = arr.size() - 1;
while (left <= right) {
contract_assert(left >= 0 && right < arr.size()); // Invariant
int mid = left + (right - left) / 2;
if (arr[mid] == target) {
return;
} else if (arr[mid] < target) {
left = mid + 1;
} else {
right = mid - 1;
}
}
}
Build Modes
4 Modes
1. ignore mode
g++ -std=c++26 -fcontracts=ignore main.cpp
- Completely ignore Contract checks
- Zero performance overhead
- Suitable for production optimized builds 2. observe mode
g++ -std=c++26 -fcontracts=observe main.cpp
- Log only on Contract violation, continue execution
- Does not terminate program
- Suitable for production monitoring 3. enforce mode
g++ -std=c++26 -fcontracts=enforce main.cpp
- Call
std::terminate()on Contract violation - Immediately terminate program
- Suitable for development/test environments 4. quick-enforce mode
g++ -std=c++26 -fcontracts=quick-enforce main.cpp
- Check only simple conditions (ignore complex ones)
- Balance performance and safety
- Minimal validation in production builds
Mode Behavior Comparison
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';
}
| Mode | Behavior |
|---|---|
| ignore | No check, undefined behavior occurs |
| observe | Log output then continue (UB may occur) |
| enforce | Call std::terminate(), terminate program |
| quick-enforce | Check only simple conditions |
Precondition
Basic Usage
#include <vector>
#include <stdexcept>
// Array index access
int get_element(const std::vector<int>& vec, size_t index)
pre(index < vec.size())
{
return vec[index];
}
// Pointer validation
void process_data(const int* data, size_t size)
pre(data != nullptr)
pre(size > 0)
{
for (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;
}
Class Methods
class BankAccount {
private:
double balance;
public:
BankAccount(double initial)
pre(initial >= 0)
: balance(initial) {}
void deposit(double amount)
pre(amount > 0)
post(balance >= old(balance)) // old(): value at function start
{
balance += amount;
}
void withdraw(double amount)
pre(amount > 0)
pre(amount <= balance)
post(balance == old(balance) - amount)
{
balance -= amount;
}
double get_balance() const
post(r: r >= 0) // Balance always non-negative
{
return balance;
}
};
Postcondition
Return Value Validation
// Named return value
int abs(int x)
post(result: result >= 0)
{
return (x < 0) ? -x : x;
}
// Multiple conditions
std::vector<int> create_range(int start, int end)
pre(start <= end)
post(result: result.size() == static_cast<size_t>(end - start + 1))
post(result: result.front() == start)
post(result: result.back() == end)
{
std::vector<int> v;
for (int i = start; i <= end; i++) {
v.push_back(i);
}
return v;
}
Assertion
contract_assert: In-Function Invariants
Unlike pre/post, which attach to the function signature and describe the contract with callers, contract_assert is placed inside the function body to validate state at a specific point — the direct successor to assert(), but subject to the same build-mode control (ignore/observe/enforce/quick-enforce) as pre/post.
#include <vector>
void insertion_sort(std::vector<int>& v) {
for (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)); // loop invariant
}
}
Difference from assert()
assert() | contract_assert | |
|---|---|---|
| Control granularity | Compile-time only (NDEBUG) | Per-build-mode (ignore/observe/enforce/quick-enforce) |
| Violation behavior | abort() | Configurable — terminate, log-and-continue, or ignore |
| Standard status | C library macro | C++26 language feature |
| Optimizer hints | None | Can inform the optimizer in ignore mode |
When to Use contract_assert vs pre/post
pre: caller’s responsibility, checked at function entrypost: implementer’s responsibility, checked at function exitcontract_assert: neither — an internal invariant that must hold at a specific point inside the function, regardless of caller/implementer boundary
double newton_sqrt(double x)
pre(x >= 0)
post(r: r >= 0)
{
double guess = x;
for (int i = 0; i < 20; i++) {
contract_assert(guess > 0); // internal invariant, not a pre/post
guess = 0.5 * (guess + x / guess);
}
return guess;
}
Practical Application Guide
Phased Introduction
Phase 1: Add preconditions to public APIs
// Library public function
void process_data(const char* data, size_t size)
pre(data != nullptr)
pre(size > 0)
{
// Implementation
}
Phase 2: Add postconditions to important functions
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(result: std::is_sorted(result.begin(), result.end()))
post(result: result.size() == a.size() + b.size())
{
// Implementation
}
Phase 3: Add invariants to complex logic
void complex_algorithm() {
// Validate initial state
contract_assert(is_valid_state());
// Perform work
step1();
contract_assert(is_valid_state());
step2();
contract_assert(is_valid_state());
step3();
contract_assert(is_valid_state());
}
Build Configuration Example
CMakeLists.txt:
# Debug build: enforce mode
set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -fcontracts=enforce")
# Release build: ignore mode
set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -fcontracts=ignore")
# RelWithDebInfo: observe mode
set(CMAKE_CXX_FLAGS_RELWITHDEBINFO "${CMAKE_CXX_FLAGS_RELWITHDEBINFO} -fcontracts=observe")
Performance Impact
Cost by Build Mode
Because Contracts checking is controlled per build mode rather than baked permanently into the binary, the runtime cost ranges from zero to comparable-to-manual-validation, entirely under your control:
| Mode | Check executes? | Overhead |
|---|---|---|
ignore | No | None — check is compiled out entirely |
quick-enforce | Simple conditions only | Low — comparable to a single branch |
observe | Yes | Same cost as the condition itself, plus logging on violation |
enforce | Yes | Same cost as the condition itself, plus std::terminate() on violation |
Compiler Optimization Opportunities
In ignore mode, the compiler is still permitted to use a pre/post condition as an optimization hint — assuming it holds even without emitting the check — the same class of benefit [[assume]] provides in C++23.
int get_element(const int* arr, size_t idx, size_t size)
pre(idx < size)
{
return arr[idx]; // compiler may assume idx < size even in ignore mode,
// potentially eliding a bounds-related branch elsewhere
}
Mixed-Mode Strategy for Hot Paths
A single build mode is not required for the whole program — validating external input at the API boundary in enforce/observe mode while keeping internal hot-path helpers in ignore mode gets safety where it matters without paying for it in the loop.
// Boundary: validate untrusted input (keep enforce/observe)
void handle_request(const Request& req)
pre(req.size <= MAX_REQUEST_SIZE)
{
process_internal(req.data, req.size);
}
// Hot path: already validated by the caller, safe to run in ignore mode
void process_internal(const int* data, size_t size)
pre(data != nullptr) // still documents intent, costs nothing in ignore mode
{
for (size_t i = 0; i < size; i++) { /* tight loop */ }
}
Comparison with Existing Approaches
Contracts vs assert vs Manual Validation vs [[assume]]
| Approach | Enabled in Release | Distinguishes pre/post | Compiler optimization hint | Configurable violation behavior |
|---|---|---|---|---|
assert() | ❌ (stripped by NDEBUG) | ❌ | ❌ | ❌ (always abort()) |
Manual if/throw | ✅ | ❌ (just a check) | ❌ | Partial (exception handling only) |
[[assume]] (C++23) | N/A (not a check) | ❌ | ✅ | N/A (undefined behavior if false) |
| C++26 Contracts | ✅ (mode-dependent) | ✅ (pre/post/contract_assert) | ✅ | ✅ (ignore/observe/enforce/quick-enforce) |
Migrating from assert
// Before: assert
int divide(int a, int b) {
assert(b != 0);
return a / b;
}
// After: Contracts — same intent, now visible in the signature
// and controllable per build mode instead of per compilation unit
int divide(int a, int b)
pre(b != 0)
{
return a / b;
}
Migrating from Manual Validation
Manual validation still has a place where you need a specific exception type and message for API consumers — Contracts violations are not catchable exceptions, they call std::terminate() (or log, in observe mode). Keep manual throw for expected, recoverable failure modes; use Contracts for conditions that should never be false in a correct program.
// Keep manual validation for expected, user-facing failures:
void withdraw(double amount) {
if (amount > balance) {
throw std::invalid_argument("Insufficient balance"); // recoverable, caller can catch
}
balance -= amount;
}
// Use Contracts for conditions that indicate a programming bug, not user error:
void internal_transfer(double amount)
pre(amount > 0) // caller bug if this is false, not a user-facing case
{
balance -= amount;
}
Troubleshooting
Problem 1: Program Terminates on Contract Violation
Symptom:
terminate called after throwing an instance of 'std::contract_violation'
Solution:
// 1. Check condition at call site
if (b != 0) {
result = divide(a, b);
}
// 2. Change to observe mode (during development)
// -fcontracts=observe
// 3. Relax condition
int divide(int a, int b)
pre(b != 0 || (std::cerr << "Warning: division by zero\n", false))
{
return b != 0 ? a / b : 0;
}
Conclusion
C++26 Contracts is an innovative feature bringing Design by Contract to language level: Key Advantages:
- Explicit contracts: Express function requirements and guarantees in code
- Build mode control: Flexible validation with ignore/observe/enforce
- Zero overhead possible: No performance impact in ignore mode
- Early bug detection: Detect contract violations during development Main Usage Patterns:
- pre: Condition before function call (caller responsibility)
- post: Condition after function return (implementer responsibility)
- contract_assert: Loop invariants, intermediate state validation Adoption Strategy:
- Start with preconditions of public APIs
- Add postconditions to important functions
- Add invariants to complex algorithms
- Optimize build modes (ignore for hot paths) Next Learning:
- C++26 Static Reflection
- C++ Exception Handling
- C++ Concepts References:
- P2900: Contracts for C++
- C++26 Feature Complete
- Design by Contract (Bertrand Meyer)
Frequently Asked Questions (FAQ)
Q. When would I use this in practice?
A. Express function preconditions, postconditions, and invariants at language level with C++26 Contracts.
Q. What should I read before this?
A. Follow the previous article or related articles links at the bottom of each post to learn in sequence. See the C++ series index for the full picture.
Q. Where can I study this more deeply?
A. Check cppreference and the relevant library’s official documentation. The reference links at the end of the article are also worth using.
Related Articles (Internal Links)
Other articles related to this topic.
- C++26 Static Reflection Complete Guide
- C++20 Concepts | Making Template Error Messages Readable
- C++26 Reflection Complete!
Keywords Covered in This Article (Related Search Terms)
This article covers C++26, Contracts, Precondition, Postcondition, Assertion, Design by Contract, Defensive Programming.