본문으로 건너뛰기 C++26 Contracts Complete Guide

C++26 Contracts Complete Guide

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

  1. What is Contracts?
  2. Basic Syntax
  3. Build Modes
  4. Precondition
  5. Postcondition
  6. Assertion
  7. Practical Patterns
  8. Performance Impact
  9. Comparison with Existing Approaches
  10. Troubleshooting
  11. 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';
}
ModeBehavior
ignoreNo check, undefined behavior occurs
observeLog output then continue (UB may occur)
enforceCall std::terminate(), terminate program
quick-enforceCheck 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 granularityCompile-time only (NDEBUG)Per-build-mode (ignore/observe/enforce/quick-enforce)
Violation behaviorabort()Configurable — terminate, log-and-continue, or ignore
Standard statusC library macroC++26 language feature
Optimizer hintsNoneCan inform the optimizer in ignore mode

When to Use contract_assert vs pre/post

  • pre: caller’s responsibility, checked at function entry
  • post: implementer’s responsibility, checked at function exit
  • contract_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:

ModeCheck executes?Overhead
ignoreNoNone — check is compiled out entirely
quick-enforceSimple conditions onlyLow — comparable to a single branch
observeYesSame cost as the condition itself, plus logging on violation
enforceYesSame 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]]

ApproachEnabled in ReleaseDistinguishes pre/postCompiler optimization hintConfigurable 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:
  1. Start with preconditions of public APIs
  2. Add postconditions to important functions
  3. Add invariants to complex algorithms
  4. Optimize build modes (ignore for hot paths) Next Learning:

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.


Other articles related to this topic.


Keywords Covered in This Article (Related Search Terms)

This article covers C++26, Contracts, Precondition, Postcondition, Assertion, Design by Contract, Defensive Programming.