C++26 Contracts Complete Guide | Language-Level Contract Programming
이 글의 핵심
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
- 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;
}
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")
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)