Skip to content

필사 모드: C++26 계약(Contracts) — 114 대 12로 통과된 기능, 그리고 GCC 16.1이라는 유일한 구현

한국어
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.

들어가며 — 만장일치가 아니었던 C++26

2026년 3월 28일 토요일, 영국 런던 크로이던에서 ISO C++ 위원회는 C++26의 기술 작업을 끝냈습니다. 여름 CD(위원회 초안) 국제 투표에서 올라온 411건의 국가기구(NB) 코멘트를 마지막까지 처리한 뒤, 최종 승인 투표(DIS)로 보낼 문서를 확정했습니다.

그런데 마지막 본회의 표결은 만장일치가 아니었습니다.

찬성 114, 반대 12, 기권 3

허브 서터(Herb Sutter)는 크로이던 트립 리포트에서 이 비만장일치가 "주로 계약에 대한 지속된 우려 때문"이라고 적었습니다. 비교 대상이 있습니다. 2025년 2월 하겐베르크에서 계약을 C++26 작업 초안에 넣을 때의 표결은 찬성 100, 반대 14, 기권 12였습니다. 1년 사이에 반대는 14에서 12로 거의 그대로인데, 기권이 12에서 3으로 줄었습니다. 서터의 해석은 이렇습니다 — 기권이 이례적으로 적다는 건 전문가 대부분이 찬성이든 반대든 이제 자기 기술적 판단을 확신한다는 뜻이다.

표준 기능이 이 단계까지 와서 두 자릿수 반대표를 받는 일은 흔치 않습니다. 무슨 일이 있었는지, 그리고 지금 이 기능을 쓸 수 있는지를 정리해 보겠습니다.

참고로 이 글에서 서터의 트립 리포트는 표결 숫자의 출처로만 씁니다. 그는 계약 찬성측이고(P3911R2의 공저자이기도 합니다), 해석과 프레이밍은 당사자의 것입니다. 논증 자체는 양측이 쓴 원 논문에서 직접 가져왔습니다.

계약이 무엇인가 — 3분 요약

계약(contract assertion)은 함수의 사전조건·사후조건과 본문 중간의 단언을 언어 문법으로 적는 기능입니다. 형태는 세 가지입니다.

// 사전조건: 호출자가 지켜야 하는 것
int divide(int a, int b)
  pre (b != 0);

// 사후조건: 함수가 보장하는 것. 반환값에 이름을 붙일 수 있다
int abs_value(int x)
  post (r: r >= 0);

// 본문 안의 단언문
void process(std::span<int> data) {
  contract_assert(!data.empty());
  // ...
}

C의 assert 매크로와 견주면 차이가 분명합니다. assert는 전처리기 매크로라 함수 선언에 붙을 수 없습니다. 사전조건은 본문에나 적을 수 있고, 그래서 헤더만 보는 호출자에게는 보이지 않습니다. 계약은 선언에 붙으므로 인터페이스의 일부가 되고, 도구가 읽을 수 있으며, 사후조건은 반환값을 이름으로 참조할 수 있습니다.

여기까지는 이견이 없습니다. 논쟁은 다음 지점에서 시작됩니다.

핵심 설계 — 검사할지 말지는 소스 코드가 정하지 않는다

C++26 표준 초안 [basic.contract.eval]은 평가 시맨틱을 넷으로 정의합니다.

  • ignore — 아무 효과 없음
  • observe — 검사하고, 실패하면 핸들러를 부르고, 핸들러가 반환하면 계속 실행
  • enforce — 검사하고, 실패하면 핸들러를 부르고, 반환하면 실행을 종료
  • quick-enforce — 검사하고, 실패하면 핸들러 없이 즉시 종료

observe·enforce·quick-enforce가 검사 시맨틱(checking semantics)이고, enforce·quick-enforce가 종료 시맨틱(terminating semantics)입니다. 그리고 다음 문장이 이 기능의 성격을 결정합니다.

It is implementation-defined which evaluation semantic is used for any given evaluation of a contract assertion.

[basic.contract.eval]/2

어떤 계약 단언이 실제로 검사될지는 구현 정의입니다. 소스 코드에 적힌 것이 아니라 컴파일러와 빌드 플래그가 정합니다. 바로 뒤 주석은 한술 더 뜹니다.

The evaluation semantics can differ for different evaluations of the same contract assertion, including evaluations during constant evaluation.

같은 계약 단언이라도 평가할 때마다 시맨틱이 다를 수 있습니다. 표준은 "권장 사항(Recommended practice)"으로 전부 ignore로 번역하는 옵션과 전부 enforce로 번역하는 옵션을 제공하고 기본값은 enforce가 되어야 한다고 적어 두었지만, 이건 권고이지 요구가 아닙니다.

여기에 하나 더 있습니다.

The evaluation A of a contract assertion using a checking semantic determines the value of the predicate. It is unspecified whether the predicate is evaluated.

검사 시맨틱을 써도 술어(predicate)가 실제로 평가되는지는 미지정입니다. 표준이 직접 든 예가 이겁니다.

struct S {
  mutable int g = 5;
} s;

void f()
  pre ((s.g++, false));   // #1

void g() {
  f();   // #1이 검사 시맨틱을 쓰더라도 s.g 증가가 일어나지 않을 수 있다
}

부수효과가 있는 술어를 쓰면 그 부수효과가 일어날 수도, 안 일어날 수도 있습니다. 구현은 같은 값을 내되 부수효과가 없는 대체 평가를 수행해도 됩니다.

찬성측 논문 P3846R0은 계약을 세 가지 특성으로 정의합니다 — 프로그램의 정확성에 잉여(redundant)이고, 독립적으로 검사 가능하며, 평가 시맨틱이 소스 코드와 독립적으로 결정된다. 세 번째가 의도된 설계입니다. 버그가 아닙니다.

반대측의 논증 — 헤더의 inline 함수 하나

반대측 논문의 제목은 완곡하지 않습니다. P3835R0 "Contracts make C++ less safe -- full stop", 존 스파이서(EDG), 빌레 보우틸라이넨, 호세 다니엘 가르시아 산체스, 2025년 9월 3일. 논증의 핵심은 코드 예제 하나입니다.

// z.h  — 서드파티 라이브러리 헤더
inline void f(int x) {
  contract_assert(x > 0);
}

// l1.cpp — 라이브러리 구현. quick_enforce로 컴파일된다
#include "z.h"
void g() {
  f(0);   // 계약 위반이 검출되기를 기대
}

// c1.cpp — 클라이언트 코드. ignore로 컴파일된다
#include "z.h"
extern void g();
int main() {
  f(1);
  g();
}

라이브러리는 자기 코드가 항상 quick_enforce로 검사되도록 빌드했습니다. 클라이언트는 ignore로 빌드합니다. finline 함수이므로 ODR상 정의는 하나만 살아남습니다. 그래서 링크 후 실제로 어떤 시맨틱이 적용될지 — 라이브러리가 의도한 검사가 살아 있을지 — 는 컴파일러가 l1.cpp에서 호출을 인라인했는지, 링커가 어느 TU의 복사본을 골랐는지에 달립니다. 논문의 문장 그대로 옮기면, 프로그래머는 어떤 호출에 어떤 시맨틱이 적용될지 알 방법이 없습니다.

이 문제는 헤더에 계약 단언을 쓰는 inline 함수나 템플릿 정의가 있으면 언제든 발생합니다. 즉 헤더 전용 라이브러리 전부입니다. 모듈도 이 문제를 풀어 주지 않는다고 논문은 적습니다. 결론은 이렇습니다.

Contracts reduce the safety of C++ and should be removed until a version that is strictly an improvement in safety is provided.

같은 방향의 논문이 더 있습니다. P3829R0 "Contracts do not belong in the language"(데이비드 치스널, 스파이서, 가브리엘 도스 레이스, 보우틸라이넨, 가르시아), 그리고 P3573R0 "Contract concerns"에는 비야네 스트롭스트룹과 다비드 반데보르드가 이름을 올렸습니다. 가벼운 반대가 아니었다는 뜻입니다.

국가기구 코멘트도 붙었습니다. P3846R0의 부록이 정리한 계약 관련 NB 코멘트는 17건이고, 스웨덴·스페인·미국·프랑스·루마니아·체코·핀란드 7개 국가기구에서 왔습니다. 그중 스페인의 ES-046은 이렇게 요약됩니다 — 중대한 검사가 제거될 수 있고, 이는 새로운 공급망 공격을 가능하게 하는 보안 취약점을 연다. 여러 건이 명시적으로 C++26에서의 제거를 요구했습니다.

이 "공급망 공격"이라는 표현이 과장처럼 들린다면, P3829R0이 든 예를 보는 게 낫습니다. P3846R0이 요약한 그대로 옮기면 이렇습니다 — 어떤 함수가 한 TU에서는 quick_enforce로, 다른 TU에서는 ignore로 컴파일된다. quick_enforce 쪽을 최적화할 때 GCC는 계약 검사가 강제된다고 가정하고 호출자 측의 널 검사를 제거한다. 그런데 링크 시점에 ignore 버전이 선택될 수 있다. 그러면 계약 검사도 없고, 그 검사를 믿고 지웠던 널 검사도 없습니다. 검사가 하나 사라진 게 아니라 두 개가 사라집니다. P3829R0은 이를 P2900의 설계 결함으로 규정하고, 해결하려면 (1) 혼합 모드를 ODR 위반으로 만들거나 (2) 컴파일러 중간표현을 크게 바꾸거나 (3) 핵심 최적화를 포기해야 한다고 주장합니다.

여기엔 찬성측의 반론이 붙어 있고, 공정하게 옮길 필요가 있습니다. P3846R0은 EWG가 하겐베르크에서 혼합 모드 관련 우려를 검토한 뒤 계약을 위해 ODR을 바꾸는 것을 거부했고, GCC의 저 프로시저 간 최적화 문제는 리플렉터에서 "계약과 무관한 컴파일러 버그"로 판정됐으며 재현 코드가 GCC에 보고됐다고 적습니다. 즉 찬성측은 이걸 언어 설계 결함이 아니라 구현 버그로 봅니다.

찬성측의 반박 — 기능적 안전과 언어 안전은 다르다

찬성측 응답이 P3846R0 "C++26 Contract Assertions, Reasserted"입니다(2025년 10월 6일). 티무르 둠러, 조슈아 버니, 가슈페르 아주만을 필두로 22명이 서명했고, 그중에는 GCC의 제이슨 메릴과 이언 샌도, 리브c++의 루이 디온, 그리고 클랭 구현을 한 에릭 피셀리어도 있습니다. 논문은 17개 "우려"를 하나씩 받아칩니다.

핵심 반박은 용어를 쪼개는 데서 시작합니다.

  • 기능적 안전(functional safety) — 프로그램이 해를 끼치지 않고 의도한 기능을 수행할 가능성
  • 언어 안전(language safety) — 미정의 동작의 부재, 또는 그 부재를 증명할 수 있는 능력

계약은 전자를 위한 물건이지 후자를 위한 물건이 아니라는 겁니다. 단언은 검사되면 버그를 잡고, 무시되면 런타임 효과 없이 의도를 문서화한다. 시맨틱을 외부에서 설정할 수 있는 능력은 결함이 아니라 널리 채택되기 위한 전제조건이다.

TU 간 시맨틱 불일치(P3835R0의 예제)에 대한 응답은 이렇습니다.

Mixing translation units compiled with different build flags is an inevitable consequence of the C++ compilation model. (...) With C assert, mixed mode makes programs IFNDR, and with P2900, the behaviour is limited to one of the semantics incorporated into the program, which is a significant improvement. (...) The worst case is that an assertion will go unchecked, which by design cannot introduce a new bug into an existing program.

즉 최악의 경우는 단언이 검사되지 않고 넘어가는 것이고, 그건 설계상 기존 프로그램에 새 버그를 넣지 않는다는 겁니다. 그리고 프로그램 전체에 균일한 시맨틱을 요구하면 "대규모에서 쓸 수 없는 기능"이 된다고 못박습니다. 헤더의 f가 성능 임계 루프에서 쓰이면 ignore 말고는 감당이 안 될 수 있고, 덜 검증된 상위 코드에서는 quick_enforce가 맞을 수 있다 — 이 가변성을 지원하지 못하는 단언 기능은 쓸모가 없다는 논리입니다.

prepost가 항상 검사되도록 의미를 바꾸자는 제안은 이미 도쿄 회의에서 표결에 부쳐져 강한 합의로 부결됐다는 점도 논문은 지적합니다. 대신 "반드시 검사"를 표현할 경로로 라벨(P3400R1)을 C++26 이후 확장으로 제시합니다.

양측 주장을 나란히 놓으면 사실 다투는 지점이 좁습니다. 동작 자체에는 이견이 없습니다. 검사가 사라질 수 있다는 것도, 소스에 강제할 방법이 없다는 것도 양측이 동의합니다. 갈리는 건 그게 "안전 기능이 아니다"인지 "안전을 해친다"인지입니다.

코나에서 고쳐진 것, 그리고 끝내 못 들어간 것

2025년 11월 코나 회의에서 위원회는 월·화 이틀을 계약 피드백에 썼습니다. 결과는 이렇습니다.

계약은 유지하되 버그 두 개를 고친다 — 강한 합의였습니다. 채택된 수정 중 하나가 P3878R1 "Standard library hardening should not use the 'observe' semantic"인데, 저자가 보우틸라이넨, 조너선 웨이클리, 스파이서, 스테판 T. 라바베이입니다. 반대측이 쓴 버그 수정 논문이 채택된 것이고, 이건 반대가 정치가 아니라 기술이었다는 방증이기도 합니다.

대조군이 있습니다. 같은 회의에서 trivial relocatability(P2786)는 제때 고칠 수 없는 결정적 버그가 발견되어 C++26에서 제거됐습니다. 위원회가 기능을 뺄 의지가 없어서 계약을 남긴 게 아니라는 뜻입니다. 같은 주에 하나는 뺐고 하나는 남겼습니다.

그리고 못 들어간 것이 있습니다. 코나에서 위원회는 P3911R0 "[basic.contract.eval] Make Contracts Reliably Non-Ignorable"의 첫 번째 해법 비슷한 것을 3월 회의까지 추진하기로 했습니다. 루마니아 NB 코멘트(RO 2-056)에 대한 응답으로, 소스 코드에 "이 단언은 반드시 강제돼야 한다"고 적는 문법을 넣자는 것이었습니다. 제안된 형태는 이랬습니다.

int divide(int a, int b)
  pre! (b != 0);   // 제안된 문법: 항상 강제됨. C++26에 들어가지 않았다

논문은 R2까지 갔고(2026년 1월 14일), 저자에 안드레이 알렉산드레스쿠와 허브 서터가 합류했습니다. 그런데 현재 표준 초안의 문법에는 이게 없습니다.

precondition-specifier:
    pre attribute-specifier-seq_opt ( conditional-expression )
postcondition-specifier:
    post attribute-specifier-seq_opt ( result-name-introducer_opt conditional-expression )

[dcl.contract.func]의 문법에 pre!는 없고, [basic.contract] 어디에도 "non-ignorable"이라는 말은 없습니다. C++26의 __cpp_contracts 값은 P2900R14의 202502L에서 그대로입니다. 서터의 크로이던 리포트도 "기능은 추가되지도 제거되지도 않았다"고 적었습니다.

정리하면 이렇습니다. C++26에는 어떤 계약 검사가 반드시 실행되도록 소스 코드에 적을 방법이 없습니다. 반대측 논증의 핵심이 그대로 남은 채로 표준이 나갔고, 그게 반대표 12개의 내용입니다.

덧붙여 C++26 계약에는 가상 함수에 계약을 붙일 수 없습니다. 이건 P3097R3 "Contracts for C++: virtual functions"로 C++29 초안에 들어갔고(GCC 상태표 기준 __cpp_contracts >= 202606L), GCC는 아직 구현하지 않았습니다. 인터페이스에 사전조건을 적겠다는 발상에서 가상 함수가 빠지는 건 꽤 큰 구멍입니다.

컴파일러 현실 — GCC 16.1 하나

여기가 실무자에게 가장 중요한 부분입니다. cppreference의 C++26 컴파일러 지원표에서 Contracts(P2900R14) 행을 보면 GCC 칸에 16이 적혀 있고 Clang, MSVC, Apple Clang, EDG eccp, Intel C++ 칸은 전부 비어 있습니다.

2026년 7월 현재 C++26 계약을 구현한 릴리스 컴파일러는 GCC 16 하나입니다. GCC 16.1은 2026년 4월 30일에 나왔습니다. 표준이 확정되고 한 달 뒤입니다.

쓰는 법은 이렇습니다.

# 계약은 -std=c++26 만으로는 켜지지 않는다. -fcontracts 가 필요하다
g++ -std=c++26 -fcontracts main.cpp

# 평가 시맨틱 선택 (기본값 enforce)
g++ -std=c++26 -fcontracts -fcontract-evaluation-semantic=quick_enforce main.cpp

GCC 16 매뉴얼과 소스의 옵션 정의를 같이 보면 이렇습니다.

플래그기본값
-fcontracts계약 기능 활성화꺼짐
-fcontract-evaluation-semantic=ignore / observe / enforce / quick_enforceenforce
-fcontracts-client-check=none / pre / all — 호출자 측 검사 삽입none
-fcontracts-definition-check=on / off — 피호출자 측 검사on
-fcontracts-conservative-ipa프로시저 간 최적화 제한켜짐
-fcontract-checks-outlined검사를 별도 함수로 분리꺼짐

작은 함정 하나 — 매뉴얼 본문은 값을 ignored, observed로 적어 두었지만, 실제 컴파일러가 받는 문자열은 ignore, observe, enforce, quick_enforce입니다(GCC 소스 gcc/c-family/c.opt의 열거형 정의 기준, 기본값 Init(3) = enforce). 매뉴얼을 그대로 복사해 붙이면 "unrecognized contract evaluation semantic" 에러를 봅니다.

위반 핸들러는 다음 함수를 정의해서 교체합니다.

#include <contracts>

void handle_contract_violation(const std::contracts::contract_violation& v) {
  // 로깅 등
}

이 표에서 눈여겨볼 건 -fcontracts-conservative-ipa입니다. GCC 문서의 설명을 그대로 옮기면, 이 옵션은 "인라인 함수의 본문이 어떤 TU에서 보일 때 프로시저 간 분석이 조치를 취하는 것을 막습니다. 계약 평가 조건이 TU마다 다를 수 있고 그런 조치가 잘못될 수 있기 때문입니다." 그리고 기본으로 켜져 있습니다.

앞 절에서 본 P3829R0의 널 검사 제거 시나리오가 바로 이겁니다. 반대측이 "TU마다 시맨틱이 다를 수 있으니 그걸 믿고 최적화하면 위험하다"고 한 그 성질이, GCC에서는 프로시저 간 최적화를 기본으로 보수화하는 플래그로 물화돼 있습니다. -fcontract-disable-optimized-checks의 설명에 "검사 단계가 원치 않게 제거(elision)되는 것을 피할 수 있는 최적화"라는 표현이 나오는 것도 같은 맥락입니다.

이걸 어느 쪽 증거로 읽을지는 갈립니다. 반대측은 "언어가 컴파일러에 이런 부담을 지운다"고 읽고, 찬성측은 "구현이 감당 가능한 문제이고 실제로 감당하고 있다"고 읽습니다. 다만 한 가지는 분명합니다 — 이 논쟁은 추상적인 철학 논쟁이 아니라 기본값으로 켜진 컴파일러 플래그로 존재하는 물건입니다.

참고로 같은 GCC 16이 리플렉션(P2996R13)도 구현했는데 그쪽은 -freflection이 따로 필요합니다. 그리고 초기화되지 않은 읽기의 미정의 동작을 없앤 P2795R5(erroneous behavior)도 GCC 16에 들어갔습니다 — 이건 계약과 달리 논쟁이 없었고, 재컴파일만으로 이득을 봅니다.

그래서 언제 쓰고, 언제 쓰지 말아야 하나

쓰지 말아야 하는 경우부터.

  • 신뢰 경계 검증에 쓰지 마십시오. 이게 제일 중요합니다. 사용자 입력, 네트워크에서 온 데이터, 권한 검사 — 이런 건 계약으로 적으면 안 됩니다. ignore로 빌드하면 사라지고, 헤더의 inline 함수라면 당신 의도와 무관하게 사라질 수 있습니다. 신뢰 경계 검사는 if와 명시적 에러 처리로 쓰십시오. P3846조차 "무시 불가능한 검사를 위한 메커니즘은 이미 있다 — 예컨대 if 문"이라고 적고 있습니다. 이건 반대측 주장이 아니라 찬성측 논문의 문장입니다.
  • 부수효과가 있는 술어를 쓰지 마십시오. 평가될지 미지정입니다.
  • 이식성이 필요한 코드에 쓰지 마십시오. 2026년 7월 현재 GCC 16 전용 기능입니다. Clang도 MSVC도 릴리스에 없습니다.
  • 가상 함수 인터페이스에는 아직 못 씁니다. C++29를 기다려야 합니다.
  • 술어 안에서 바깥 변수는 const가 되고 thisconst 포인터가 됩니다(const화). 비-const 연산을 부르는 검사는 그대로 안 넘어갑니다.

값을 하는 경우.

  • 내부 코드베이스에서 사전조건을 인터페이스에 문서화하는 용도. 헤더만 보고 계약을 알 수 있다는 건 assert가 못 하던 일이고, 이건 ignore로 빌드해도 남는 이득입니다.
  • 디버그·테스트 빌드에서 enforce, 릴리스에서 ignore로 돌리는 전형적인 패턴. 이건 assert로 하던 걸 더 나은 문법으로 하는 것이고, 실제로 대부분의 초기 채택이 여기일 겁니다.
  • 단일 컴파일러(GCC)로 빌드하는 것이 확정된 내부 서비스에서, 전 TU를 같은 시맨틱으로 컴파일하는 경우. P3846이 "자연스러운 초기 전략"이라 부른 것이 정확히 이겁니다 — TU마다 단일 시맨틱으로 컴파일하기. 혼합 모드를 안 쓰면 P3835R0의 문제는 애초에 안 생깁니다.

당신이 빌드 전체를 통제하면 계약은 잘 동작합니다. 문제는 서드파티 라이브러리를 헤더로 받아 쓰는 순간, 그리고 라이브러리 저자가 "내 검사는 항상 켜져 있어야 한다"고 말하고 싶어지는 순간에 생깁니다. C++26은 그 말을 소스 코드에 적을 문법을 주지 않았습니다.

마치며

정리하면 이렇습니다. C++26 계약은 2026년 3월 28일에 찬성 114, 반대 12, 기권 3으로 표준에 남았습니다. 20년 논의와 SG21의 5년 작업, 그리고 C++20에서 한 번 빠졌던 이력 끝에 나온 결과입니다. 기능 자체는 진짜입니다 — 선언에 붙는 사전·사후조건은 assert가 못 하던 일을 하고, 네 가지 평가 시맨틱은 실무의 다양한 요구를 반영합니다.

동시에 반대측 논증도 진짜입니다. 검사가 실행될지를 소스 코드가 정하지 못하고, 헤더의 inline 함수에서는 어떤 시맨틱이 적용될지 프로그래머가 알 수 없습니다. 이건 버그가 아니라 의도된 설계이고, 그래서 "고쳐질" 성질의 것이 아닙니다. 소스에 강제를 적는 문법(P3911)은 3월까지 추진됐지만 C++26에 들어가지 못했고, 라벨(P3400)은 C++26 이후 과제로 남았습니다.

그러니 이 기능을 대할 때의 태도는 분명합니다. 계약은 안전 기능이 아니라 버그 검출 도구입니다. 찬성측조차 그렇게 말합니다 — 기능적 안전을 위한 것이지 언어 안전을 위한 것이 아니라고. 이 구분을 놓치고 "C++26은 계약으로 안전해진다"고 읽으면, 정확히 반대측이 경고한 방식으로 다치게 됩니다. 무시될 수 있는 검사를 신뢰 경계에 놓는 것이니까요.

그리고 2026년 7월 현재, 쓰고 싶어도 GCC 16.1이 유일한 선택지입니다. 서터는 C++26 채택이 빠를 거라 예상하지만, 계약에 관한 한 지금 할 수 있는 일은 GCC 16으로 실험해 보는 것까지입니다. 이식 가능한 프로덕션 코드에 넣는 건 Clang과 MSVC가 따라온 뒤의 이야기입니다.

참고 자료

현재 단락 (1/133)

2026년 3월 28일 토요일, 영국 런던 크로이던에서 ISO C++ 위원회는 C++26의 기술 작업을 끝냈습니다. 여름 CD(위원회 초안) 국제 투표에서 올라온 411건의 국가기...

작성 글자: 0원문 글자: 12,231작성 단락: 0/133