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++ 함수 기본
- 예외 처리 이해 (예외 가이드)
- 디버깅 경험
목차
- Contracts란?
- 기본 문법
- 빌드 모드
- 사전조건 (Precondition)
- 사후조건 (Postcondition)
- 불변식 (Assertion)
- 실전 패턴
- 성능 영향
- 기존 방식과 비교
- 마무리
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 발생 가능) |
| enforce | std::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: 루프 불변식, 중간 상태 검증
도입 전략:
- 공개 API의 사전조건부터 시작
- 중요 함수에 사후조건 추가
- 복잡한 알고리즘에 불변식 추가
- 빌드 모드 최적화 (핫 패스는 ignore)
다음 학습:
- C++26 Static Reflection
- C++ 예외 처리
- C++ Concepts
참고 자료: