C++26 Contracts 완벽 가이드 | 언어 레벨 계약 프로그래밍

C++26 Contracts 완벽 가이드 | 언어 레벨 계약 프로그래밍

이 글의 핵심

C++26 Contracts의 핵심 개념과 실전 활용법. pre, post, contract_assert 문법, 빌드 모드(ignore/observe/enforce), 성능 영향, 실무 패턴을 코드 예제와 함께 설명합니다.

들어가며

C++26의 Contracts는 함수의 사전조건(precondition), 사후조건(postcondition), 불변식(invariant)언어 레벨에서 표현하는 기능입니다. 기존에는 assert, 주석, 또는 수동 검증 코드로 처리하던 것을 표준 문법으로 명시하고, 컴파일러 플래그로 검증 수준을 제어할 수 있습니다.

이 글은 Contracts의 기본 문법 (pre, post, contract_assert), 빌드 모드, 실전 패턴, 기존 방식과의 비교를 코드 예제와 함께 설명합니다.

학습 전제 조건:

  • C++ 함수 기본
  • 예외 처리 이해 (예외 가이드)
  • 디버깅 경험

목차

  1. Contracts란?
  2. 기본 문법
  3. 빌드 모드
  4. 사전조건 (Precondition)
  5. 사후조건 (Postcondition)
  6. 불변식 (Assertion)
  7. 실전 패턴
  8. 성능 영향
  9. 기존 방식과 비교
  10. 마무리

Contracts란?

Design by Contract

Contracts는 Bertrand Meyer의 “Design by Contract” 개념을 C++에 도입한 것입니다:

함수 = 계약
- 사전조건 (Precondition): 호출자가 보장해야 할 것
- 사후조건 (Postcondition): 함수가 보장할 것
- 불변식 (Invariant): 항상 참이어야 할 것

기존 방식의 한계

assert 사용:

#include <cassert>

int divide(int a, int b) {
    assert(b != 0);  // 디버그 빌드에서만 동작
    return a / b;
}
  • Release 빌드에서는 무시됨
  • 사전조건인지 불변식인지 불명확
  • 컴파일러 최적화에 힌트 제공 안 됨

수동 검증:

int divide(int a, int b) {
    if (b == 0) {
        throw std::invalid_argument("Division by zero");
    }
    return a / b;
}
  • 성능 오버헤드 (항상 검사)
  • 예외 처리 복잡도 증가
  • 계약 의도가 명확하지 않음

C++26 Contracts:

int divide(int a, int b)
    pre(b != 0)  // 사전조건 명시
{
    return a / b;
}
  • 의도 명확
  • 빌드 모드로 검증 수준 제어
  • 컴파일러 최적화 가능

기본 문법

사전조건: pre

// 함수 선언 뒤에 사전조건 명시
int sqrt_int(int x)
    pre(x >= 0)  // x는 음수가 아니어야 함
{
    // 구현
    return static_cast<int>(std::sqrt(x));
}

// 여러 조건
void process(int* ptr, int size)
    pre(ptr != nullptr)
    pre(size > 0)
{
    for (int i = 0; i < size; i++) {
        ptr[i] *= 2;
    }
}

사후조건: post

int factorial(int n)
    pre(n >= 0)
    post(r: r > 0)  // 반환값 r은 양수여야 함
{
    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;
}

불변식: 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());  // 불변식
        
        int mid = left + (right - left) / 2;
        
        if (arr[mid] == target) {
            return;
        } else if (arr[mid] < target) {
            left = mid + 1;
        } else {
            right = mid - 1;
        }
    }
}

빌드 모드

4가지 모드

1. ignore 모드

g++ -std=c++26 -fcontracts=ignore main.cpp
  • Contract 검사 완전히 무시
  • 성능 오버헤드 제로
  • 프로덕션 최적화 빌드에 적합

2. observe 모드

g++ -std=c++26 -fcontracts=observe main.cpp
  • Contract 위반 시 로그만 남기고 계속 실행
  • 프로그램 종료하지 않음
  • 프로덕션 모니터링에 적합

3. enforce 모드

g++ -std=c++26 -fcontracts=enforce main.cpp
  • Contract 위반 시 std::terminate() 호출
  • 프로그램 즉시 종료
  • 개발/테스트 환경에 적합

4. quick-enforce 모드

g++ -std=c++26 -fcontracts=quick-enforce main.cpp
  • 간단한 조건만 검사 (복잡한 조건 무시)
  • 성능과 안전성 균형
  • 프로덕션 빌드에서 최소 검증

모드별 동작 비교

int divide(int a, int b)
    pre(b != 0)
{
    return a / b;
}

int main() {
    int result = divide(10, 0);  // Contract 위반
    std::cout << result << '\n';
}
모드동작
ignore검사 안 함, 정의되지 않은 동작 발생
observe로그 출력 후 계속 실행 (UB 발생 가능)
enforcestd::terminate() 호출, 프로그램 종료
quick-enforce간단한 조건만 검사

사전조건 (Precondition)

기본 사용

#include <vector>
#include <stdexcept>

// 배열 인덱스 접근
int get_element(const std::vector<int>& vec, size_t index)
    pre(index < vec.size())
{
    return vec[index];
}

// 포인터 검증
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] << ' ';
    }
}

// 범위 검증
double calculate_percentage(int part, int total)
    pre(total > 0)
    pre(part >= 0 && part <= total)
{
    return (static_cast<double>(part) / total) * 100.0;
}

복잡한 조건

#include <algorithm>

// 정렬 여부 확인
int binary_search(const std::vector<int>& arr, int target)
    pre(std::is_sorted(arr.begin(), arr.end()))
{
    auto it = std::lower_bound(arr.begin(), arr.end(), target);
    return (it != arr.end() && *it == target) ? 
           std::distance(arr.begin(), it) : -1;
}

// 커스텀 검증 함수
bool is_valid_email(const std::string& email) {
    return email.find('@') != std::string::npos;
}

void send_email(const std::string& email, const std::string& message)
    pre(is_valid_email(email))
    pre(!message.empty())
{
    // 이메일 전송 로직
}

클래스 메서드

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(): 함수 시작 시점의 값
    {
        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)  // 잔액은 항상 음수가 아님
    {
        return balance;
    }
};

사후조건 (Postcondition)

반환값 검증

// 반환값 이름 지정
int abs(int x)
    post(result: result >= 0)
{
    return (x < 0) ? -x : x;
}

// 여러 조건
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;
}

old() 함수 (이전 값 참조)

class Counter {
private:
    int count = 0;
    
public:
    void increment()
        post(count == old(count) + 1)  // 정확히 1 증가
    {
        count++;
    }
    
    void add(int n)
        pre(n >= 0)
        post(count == old(count) + n)
    {
        count += n;
    }
    
    void reset()
        post(count == 0)
    {
        count = 0;
    }
};

복잡한 사후조건

#include <algorithm>

// 정렬 보장
std::vector<int> sort_and_deduplicate(std::vector<int> v)
    post(result: std::is_sorted(result.begin(), result.end()))
    post(result: std::adjacent_find(result.begin(), result.end()) == result.end())  // 중복 없음
    post(result: result.size() <= v.size())
{
    std::sort(v.begin(), v.end());
    v.erase(std::unique(v.begin(), v.end()), v.end());
    return v;
}

// 불변 속성 유지
std::vector<int> filter_positive(const std::vector<int>& v)
    post(result: std::all_of(result.begin(), result.end(), 
                             [](int x) { return x > 0; }))
{
    std::vector<int> result;
    std::copy_if(v.begin(), v.end(), std::back_inserter(result),
                 [](int x) { return x > 0; });
    return result;
}

불변식 (Assertion)

contract_assert 사용

void process_array(int* arr, int size)
    pre(arr != nullptr)
    pre(size > 0)
{
    for (int i = 0; i < size; i++) {
        contract_assert(i >= 0 && i < size);  // 루프 불변식
        
        arr[i] *= 2;
        
        contract_assert(arr[i] % 2 == 0);  // 결과 검증
    }
}

// 복잡한 자료구조 불변식
class BinarySearchTree {
private:
    struct Node {
        int value;
        Node* left;
        Node* right;
    };
    
    Node* root;
    
    bool is_valid_bst(Node* node, int min_val, int max_val) const {
        if (!node) return true;
        if (node->value <= min_val || node->value >= max_val) return false;
        return is_valid_bst(node->left, min_val, node->value) &&
               is_valid_bst(node->right, node->value, max_val);
    }
    
public:
    void insert(int value) {
        // 삽입 로직
        
        contract_assert(is_valid_bst(root, INT_MIN, INT_MAX));  // BST 속성 유지
    }
};

실전 패턴

1. 배열/벡터 안전 접근

template<typename T>
class SafeVector {
private:
    std::vector<T> data;
    
public:
    void push_back(const T& value) {
        data.push_back(value);
    }
    
    T& at(size_t index)
        pre(index < data.size())
        post(result: &result >= data.data() && 
                     &result < data.data() + data.size())
    {
        return data[index];
    }
    
    const T& at(size_t index) const
        pre(index < data.size())
    {
        return data[index];
    }
    
    size_t size() const
        post(result: result == data.size())
    {
        return data.size();
    }
};

2. 리소스 관리

class FileHandle {
private:
    FILE* file = nullptr;
    
    bool is_open() const {
        return file != nullptr;
    }
    
public:
    void open(const char* filename)
        pre(filename != nullptr)
        pre(!is_open())
        post(is_open())
    {
        file = fopen(filename, "r");
        if (!file) {
            throw std::runtime_error("Failed to open file");
        }
    }
    
    void close()
        pre(is_open())
        post(!is_open())
    {
        if (file) {
            fclose(file);
            file = nullptr;
        }
    }
    
    size_t read(char* buffer, size_t size)
        pre(is_open())
        pre(buffer != nullptr)
        pre(size > 0)
        post(result: result <= size)
    {
        return fread(buffer, 1, size, file);
    }
    
    ~FileHandle() {
        if (is_open()) {
            close();
        }
    }
};

3. 수학 함수

#include <cmath>
#include <limits>

double safe_sqrt(double x)
    pre(x >= 0)
    post(result: result >= 0)
    post(result: std::abs(result * result - x) < 0.0001)  // 정확도 검증
{
    return std::sqrt(x);
}

double safe_log(double x)
    pre(x > 0)
    post(result: !std::isnan(result))
    post(result: !std::isinf(result))
{
    return std::log(x);
}

int safe_factorial(int n)
    pre(n >= 0)
    pre(n <= 12)  // int 오버플로우 방지
    post(result: result > 0)
{
    int result = 1;
    for (int i = 2; i <= n; i++) {
        contract_assert(result <= INT_MAX / i);  // 오버플로우 검사
        result *= i;
    }
    return result;
}

4. 문자열 처리

std::string substring(const std::string& str, size_t pos, size_t len)
    pre(pos < str.size())
    pre(len > 0)
    post(result: result.size() <= len)
    post(result: result.size() <= str.size() - pos)
{
    return str.substr(pos, len);
}

std::string to_uppercase(std::string str)
    post(result: result.size() == str.size())
    post(result: std::all_of(result.begin(), result.end(), 
                             [](char c) { return !std::islower(c); }))
{
    std::transform(str.begin(), str.end(), str.begin(), ::toupper);
    return str;
}

5. 컨테이너 불변식

template<typename T>
class Stack {
private:
    std::vector<T> data;
    
    bool is_valid() const {
        return data.capacity() >= data.size();
    }
    
public:
    void push(const T& value)
        post(size() == old(size()) + 1)
        post(top() == value)
    {
        data.push_back(value);
        contract_assert(is_valid());
    }
    
    T pop()
        pre(!empty())
        post(size() == old(size()) - 1)
    {
        T value = data.back();
        data.pop_back();
        contract_assert(is_valid());
        return value;
    }
    
    const T& top() const
        pre(!empty())
    {
        return data.back();
    }
    
    bool empty() const
        post(result: result == (size() == 0))
    {
        return data.empty();
    }
    
    size_t size() const
        post(result: result == data.size())
    {
        return data.size();
    }
};

성능 영향

벤치마크 (참고용)

간단한 조건 (x != 0):

int divide(int a, int b)
    pre(b != 0)
{
    return a / b;
}
  • ignore 모드: 0% 오버헤드
  • quick-enforce 모드: ~1-2% 오버헤드
  • enforce 모드: ~2-5% 오버헤드

복잡한 조건 (정렬 검증):

int binary_search(const std::vector<int>& arr, int target)
    pre(std::is_sorted(arr.begin(), arr.end()))
{
    // ...
}
  • ignore 모드: 0% 오버헤드
  • quick-enforce 모드: 검사 안 함 (복잡도 높음)
  • enforce 모드: O(N) 검사 → 큰 오버헤드

최적화 전략

1. 핫 패스는 ignore 모드

// 성능 크리티컬한 내부 함수
inline int fast_add(int a, int b)
    [[likely_ignore]]  // 힌트: ignore 모드 권장
{
    return a + b;
}

2. 공개 API는 enforce 모드

// 외부 입력 검증
void public_api(const std::string& input)
    [[likely_enforce]]  // 힌트: enforce 모드 권장
    pre(!input.empty())
    pre(input.size() <= 1000)
{
    // 처리
}

3. 조건 복잡도 최소화

// ❌ 비싼 조건
void process(const std::vector<int>& v)
    pre(std::is_sorted(v.begin(), v.end()))  // O(N)
    pre(std::all_of(v.begin(), v.end(), [](int x) { return x > 0; }))  // O(N)
{
    // ...
}

// ✅ 간단한 조건으로 분리
void process(const std::vector<int>& v)
    pre(!v.empty())  // O(1)
{
    contract_assert(std::is_sorted(v.begin(), v.end()));  // 디버그 빌드만
    // ...
}

기존 방식과 비교

assert vs Contracts

assert (C++03):

#include <cassert>

int divide(int a, int b) {
    assert(b != 0);  // NDEBUG 정의 시 무시
    return a / b;
}
  • Debug 빌드만 동작
  • Release 빌드에서 완전히 제거
  • 사전/사후조건 구분 없음

Contracts (C++26):

int divide(int a, int b)
    pre(b != 0)
{
    return a / b;
}
  • 빌드 모드로 세밀한 제어
  • 사전/사후조건 명시적 구분
  • 컴파일러 최적화 힌트

예외 vs Contracts

예외 처리:

int divide(int a, int b) {
    if (b == 0) {
        throw std::invalid_argument("Division by zero");
    }
    return a / b;
}

// 호출부
try {
    int result = divide(10, 0);
} catch (const std::exception& e) {
    std::cerr << e.what() << '\n';
}
  • 런타임 검사 (항상 실행)
  • 예외 처리 오버헤드
  • 복구 가능

Contracts:

int divide(int a, int b)
    pre(b != 0)
{
    return a / b;
}

// 호출부
int result = divide(10, 0);  // Contract 위반 → terminate
  • 빌드 모드에 따라 검사
  • 성능 오버헤드 최소
  • 복구 불가 (프로그램 종료)

언제 무엇을 사용?

상황권장
프로그래머 오류 (버그)Contracts
외부 입력 검증예외
복구 가능한 에러예외
성능 크리티컬Contracts (ignore 모드)

고급 패턴

1. 클래스 불변식

class CircularBuffer {
private:
    std::vector<int> buffer;
    size_t capacity;
    size_t head = 0;
    size_t tail = 0;
    size_t count = 0;
    
    bool is_valid() const {
        return count <= capacity &&
               head < capacity &&
               tail < capacity &&
               buffer.size() == capacity;
    }
    
public:
    CircularBuffer(size_t cap)
        pre(cap > 0)
        post(is_valid())
        : capacity(cap), buffer(cap) {}
    
    void push(int value)
        pre(!full())
        post(size() == old(size()) + 1)
        post(is_valid())
    {
        buffer[tail] = value;
        tail = (tail + 1) % capacity;
        count++;
        
        contract_assert(is_valid());
    }
    
    int pop()
        pre(!empty())
        post(size() == old(size()) - 1)
        post(is_valid())
    {
        int value = buffer[head];
        head = (head + 1) % capacity;
        count--;
        
        contract_assert(is_valid());
        return value;
    }
    
    bool empty() const
        post(result: result == (count == 0))
    {
        return count == 0;
    }
    
    bool full() const
        post(result: result == (count == capacity))
    {
        return count == capacity;
    }
    
    size_t size() const
        post(result: result <= capacity)
    {
        return count;
    }
};

2. 알고리즘 불변식

// 퀵소트 불변식
void quicksort(std::vector<int>& arr, int low, int high)
    pre(low >= 0)
    pre(high < static_cast<int>(arr.size()))
    post(std::is_sorted(arr.begin() + low, arr.begin() + high + 1))
{
    if (low >= high) return;
    
    int pivot = arr[high];
    int i = low - 1;
    
    for (int j = low; j < high; j++) {
        contract_assert(j >= low && j < high);  // 루프 불변식
        
        if (arr[j] < pivot) {
            i++;
            std::swap(arr[i], arr[j]);
        }
    }
    
    std::swap(arr[i + 1], arr[high]);
    int pi = i + 1;
    
    contract_assert(pi >= low && pi <= high);
    
    quicksort(arr, low, pi - 1);
    quicksort(arr, pi + 1, high);
}

// 이진 탐색 불변식
int binary_search(const std::vector<int>& arr, int target)
    pre(std::is_sorted(arr.begin(), arr.end()))
    post(result: result == -1 || 
                 (result >= 0 && result < static_cast<int>(arr.size()) && 
                  arr[result] == target))
{
    int left = 0, right = arr.size() - 1;
    
    while (left <= right) {
        contract_assert(left >= 0 && right < static_cast<int>(arr.size()));
        contract_assert(left <= right + 1);
        
        int mid = left + (right - left) / 2;
        
        if (arr[mid] == target) {
            return mid;
        } else if (arr[mid] < target) {
            left = mid + 1;
        } else {
            right = mid - 1;
        }
    }
    
    return -1;
}

3. 동시성 불변식

#include <mutex>
#include <thread>

class ThreadSafeCounter {
private:
    mutable std::mutex mtx;
    int count = 0;
    
    bool is_locked() const {
        // 뮤텍스가 현재 스레드에 의해 잠겨있는지 확인
        // (실제 구현은 더 복잡)
        return true;  // 간소화
    }
    
public:
    void increment()
        post(count == old(count) + 1)
    {
        std::lock_guard<std::mutex> lock(mtx);
        contract_assert(is_locked());
        
        count++;
    }
    
    int get() const {
        std::lock_guard<std::mutex> lock(mtx);
        contract_assert(is_locked());
        
        return count;
    }
};

4. 스마트 포인터 래퍼

template<typename T>
class NonNullPtr {
private:
    T* ptr;
    
public:
    explicit NonNullPtr(T* p)
        pre(p != nullptr)
        post(ptr != nullptr)
        : ptr(p) {}
    
    T& operator*()
        pre(ptr != nullptr)
    {
        return *ptr;
    }
    
    T* operator->()
        pre(ptr != nullptr)
        post(result: result != nullptr)
    {
        return ptr;
    }
    
    T* get()
        pre(ptr != nullptr)
        post(result: result != nullptr)
    {
        return ptr;
    }
};

실무 적용 가이드

단계별 도입

1단계: 공개 API에 사전조건 추가

// 라이브러리 공개 함수
void process_data(const char* data, size_t size)
    pre(data != nullptr)
    pre(size > 0)
{
    // 구현
}

2단계: 중요 함수에 사후조건 추가

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())
{
    // 구현
}

3단계: 복잡한 로직에 불변식 추가

void complex_algorithm() {
    // 초기 상태 검증
    contract_assert(is_valid_state());
    
    // 작업 수행
    step1();
    contract_assert(is_valid_state());
    
    step2();
    contract_assert(is_valid_state());
    
    step3();
    contract_assert(is_valid_state());
}

빌드 설정 예제

CMakeLists.txt:

# Debug 빌드: enforce 모드
set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -fcontracts=enforce")

# Release 빌드: ignore 모드
set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -fcontracts=ignore")

# RelWithDebInfo: observe 모드
set(CMAKE_CXX_FLAGS_RELWITHDEBINFO "${CMAKE_CXX_FLAGS_RELWITHDEBINFO} -fcontracts=observe")

트러블슈팅

문제 1: Contract 위반 시 프로그램 종료

증상:

terminate called after throwing an instance of 'std::contract_violation'

해결:

// 1. 호출부에서 조건 확인
if (b != 0) {
    result = divide(a, b);
}

// 2. observe 모드로 변경 (개발 중)
// -fcontracts=observe

// 3. 조건 완화
int divide(int a, int b)
    pre(b != 0 || (std::cerr << "Warning: division by zero\n", false))
{
    return b != 0 ? a / b : 0;
}

문제 2: 복잡한 조건으로 빌드 느림

증상:

void process(const std::vector<int>& v)
    pre(std::all_of(v.begin(), v.end(), is_complex_condition))  // 매우 느림
{
    // ...
}

해결:

// 1. 조건 간소화
void process(const std::vector<int>& v)
    pre(!v.empty())  // 간단한 조건만
{
    contract_assert(std::all_of(v.begin(), v.end(), is_complex_condition));
    // ...
}

// 2. 디버그 전용 검증
#ifdef DEBUG_CONTRACTS
    pre(std::all_of(v.begin(), v.end(), is_complex_condition))
#endif

문제 3: old() 값이 예상과 다름

증상:

void increment(int& x)
    post(x == old(x) + 1)
{
    x++;
    x++;  // ❌ 버그: 2 증가
}

해결:

  • Contract 위반이 버그를 찾아줌
  • 구현 수정 필요

마무리

C++26 Contracts는 Design by Contract를 언어 레벨로 가져온 혁신적 기능입니다:

핵심 장점:

  • 명시적 계약: 함수의 요구사항과 보장을 코드로 표현
  • 빌드 모드 제어: ignore/observe/enforce로 유연한 검증
  • 제로 오버헤드 가능: ignore 모드에서 성능 영향 없음
  • 버그 조기 발견: 개발 단계에서 계약 위반 검출

주요 사용 패턴:

  • pre: 함수 호출 전 조건 (호출자 책임)
  • post: 함수 반환 후 조건 (구현자 책임)
  • contract_assert: 루프 불변식, 중간 상태 검증

도입 전략:

  1. 공개 API의 사전조건부터 시작
  2. 중요 함수에 사후조건 추가
  3. 복잡한 알고리즘에 불변식 추가
  4. 빌드 모드 최적화 (핫 패스는 ignore)

다음 학습:

  • C++26 Static Reflection
  • C++ 예외 처리
  • C++ Concepts

참고 자료: