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
- 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)
자주 묻는 질문 (FAQ)
Q. 이 내용을 실무에서 언제 쓰나요?
A. Express function preconditions, postconditions, and invariants at language level with C++26 Contracts. From pre, post, c… 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.
Q. 선행으로 읽으면 좋은 글은?
A. 각 글 하단의 이전 글 또는 관련 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.
Q. 더 깊이 공부하려면?
A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- C++26 Static Reflection 완벽 가이드 | 컴파일 타임 타입 정보 활용
- C++20 Concepts | 템플릿 에러 메시지를 읽기 쉽게 만드는 방법
- C++26 Reflection 완성! | Static Reflection으로 메타프로그래밍 혁신
이 글에서 다루는 키워드 (관련 검색어)
C++26, Contracts, Precondition, Postcondition, Assertion, Design by Contract, Defensive Programming 등으로 검색하시면 이 글이 도움이 됩니다.