Contracts — 함수에 계약을 붙인다는 것
assert()로 20년 넘게 버텨온 이유
C++ 개발자라면 함수 맨 앞줄에 이런 코드를 셀 수 없이 써봤을 겁니다.
double sqrt_positive(double x) {
assert(x >= 0.0); // 진짜 계약이 아니라 관례일 뿐이다
return std::sqrt(x);
}이건 계약이 아니라 관례입니다. assert는 컴파일러 입장에서 그냥 평범한 매크로이고, 함수 시그니처의 일부가 아닙니다. 헤더 파일만 봐서는 이 함수가 음수를 거부하는지 알 방법이 없고, NDEBUG가 정의된 릴리스 빌드에서는 통째로 사라집니다. 무엇보다 후행조건(반환값이 어떤 조건을 만족해야 하는지)을 표현할 표준 방법이 아예 없었습니다.
C++26의 Contracts(P2900)는 이걸 언어 문법으로 끌어올립니다.
pre와 post — 함수 선언부에 계약을 붙인다
int sqrt_positive(int x)
pre(x >= 0)
post(r: r >= 0)
{
return static_cast<int>(std::sqrt(static_cast<double>(x)));
}pre(x >= 0)는 호출자가 지켜야 할 사전조건입니다. post(r: r >= 0)의 r은 반환값을 가리키는 이름이고, 함수가 끝난 뒤 그 값이 만족해야 할 후행조건을 적습니다. 둘 다 함수 선언의 일부이므로 헤더만 읽어도 이 함수의 입출력 규약을 알 수 있습니다. assert와 가장 다른 지점이 바로 이것입니다 — 계약이 구현이 아니라 인터페이스에 속하게 됩니다.
배열 접근처럼 관용적으로 많이 쓰이는 자리에도 자연스럽게 붙습니다.
class Buffer {
std::vector<int> data_;
public:
int& operator[](size_t index)
pre(index < data_.size())
{
return data_[index];
}
};contract_assert — 함수 몸통 안의 계약
pre/post는 함수 선언부 전용입니다. 함수 몸통 중간에서 뭔가를 검증하고 싶으면 contract_assert 문을 씁니다.
void process(std::vector<Order>& orders) {
contract_assert(!orders.empty());
std::ranges::sort(orders, {}, &Order::priority);
contract_assert(std::ranges::is_sorted(orders, {}, &Order::priority));
}contract_assert가 매크로가 아니라 온전한 키워드라는 점도 짚어둘 만합니다. 제안서 논의 과정에서 contract_assert(order.is_valid())처럼 괄호가 붙는 형태가 함수 호출 contract_assert(...)와 문법적으로 구분되지 않는 문제가 있었고, 그래서 키워드로 격상시켜 애매함을 없앴습니다.
세 가지 평가 시맨틱 — default, audit, axiom
계약을 붙였다고 해서 항상 런타임에 검사가 도는 건 아닙니다. Contracts는 세 가지 수준을 구분합니다.
| 수준 | 의미 | 대략적인 용도 |
|---|---|---|
default |
표준 빌드에서 평가됨 | 일반적인 사전·후행조건 |
audit |
비용이 큰 검사, 별도 빌드 설정에서만 켬 | 정렬 여부 확인처럼 O(n) 이상 드는 검사 |
axiom |
절대 평가되지 않음, 문서·최적화 힌트 목적 | "이 포인터는 항상 non-null" 같은 불변식 명시 |
int at(int index)
pre(audit: std::ranges::is_sorted(data_)) // 비용이 크므로 audit 로 지정
pre(index >= 0 && index < data_.size()) // 기본 수준
{
return data_[index];
}이 구분이 왜 필요하냐면, 계약 검사에는 실행 비용이 따르기 때문입니다. 사전조건 하나 검사하는 건 무시할 만하지만, "이 벡터가 정렬돼 있다"를 확인하려면 O(n)이 듭니다. 모든 계약을 무조건 default로 걸면 릴리스 빌드 성능이 눈에 띄게 나빠질 수 있어서, 비용이 큰 검사는 개발자가 명시적으로 audit로 분리해 필요할 때만(전수 테스트, 디버그 빌드 등) 켜도록 설계했습니다.
다음 장에서는 이 기능이 화려해 보이는 것과 별개로 실무에 끌어들이기 전에 꼭 확인해야 할 함정들 — 위반 처리 방식, 성능, 그리고 "이걸로 assert를 완전히 대체해도 되는가"라는 질문을 다룹니다.