Skip to content

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

|

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

들어가며 — 만장일치가 아니었던 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가 따라온 뒤의 이야기입니다.

참고 자료

C++26 Contracts — the feature that passed 114 to 12, and the one implementation that shipped it: GCC 16.1

Introduction — C++26 Passed, But Not Unanimously

On Saturday, March 28, 2026, in Croydon, London, the ISO C++ committee finished the technical work on C++26. After working through the last of the 411 National Body (NB) comments that came out of the summer CD (Committee Draft) international ballot, the committee finalized the document to be sent to the final approval vote (DIS).

But the final plenary vote was not unanimous.

114 in favor, 12 against, 3 abstentions

In his Croydon trip report, Herb Sutter wrote that this lack of unanimity was "mainly due to continuing concerns about contracts." There's a point of comparison. When contracts were added to the C++26 working draft in February 2025 in Hagenberg, the vote was 100 in favor, 14 against, 12 abstentions. Over the year, the "against" count barely moved, from 14 to 12, but the abstentions dropped from 12 to 3. Sutter's reading: an unusually low abstention count means most experts now feel confident in their own technical judgment, whichever side they land on.

It's rare for a standard feature to still draw a double-digit "against" count this late in the process. Let's lay out what happened, and whether you can actually use this feature today.

One note before we start: this post uses Sutter's trip report strictly as the source for the vote counts. He is on the pro-contracts side (and a co-author of P3911R2), so the interpretation and framing there are his own. The arguments themselves are drawn directly from the original papers each side wrote.

What Contracts Are — the 3-Minute Version

A contract assertion is language syntax for writing a function's preconditions, postconditions, and mid-body assertions. There are three forms.

// Precondition: what the caller must uphold
int divide(int a, int b)
  pre (b != 0);

// Postcondition: what the function guarantees. The return value can be named
int abs_value(int x)
  post (r: r >= 0);

// An assertion inside the body
void process(std::span<int> data) {
  contract_assert(!data.empty());
  // ...
}

The difference from C's assert macro is clear once you line them up. assert is a preprocessor macro, so it cannot attach to a function declaration. A precondition written that way can only live in the body, invisible to a caller who only sees the header. Contracts attach to the declaration, so they become part of the interface, tools can read them, and postconditions can refer to the return value by name.

Nobody disputes any of this. The argument starts at the next point.

The Core Design — Source Code Doesn't Decide Whether a Check Runs

The C++26 draft standard's [basic.contract.eval] defines four evaluation semantics.

  • ignore — no effect
  • observe — check it, call the handler on failure, and continue execution if the handler returns
  • enforce — check it, call the handler on failure, and terminate execution if the handler returns
  • quick-enforce — check it, and terminate immediately on failure, with no handler call

observe, enforce, and quick-enforce are the checking semantics; enforce and quick-enforce are the terminating semantics. And the following sentence is what defines the character of this feature.

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

[basic.contract.eval]/2

Whether a given contract assertion actually gets checked is implementation-defined. It's decided not by what's written in the source, but by the compiler and build flags. The very next note goes further still.

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

Even the same contract assertion can get a different semantic on different evaluations. The standard offers, as "Recommended practice," an option to translate everything as ignore and an option to translate everything as enforce, and says the default should be enforce — but that is a recommendation, not a requirement.

There's one more layer to this.

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.

Even under a checking semantic, whether the predicate is actually evaluated is unspecified. The standard's own example shows this directly.

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

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

void g() {
  f();   // even if #1 uses a checking semantic, the increment of s.g may not happen
}

If your predicate has a side effect, that side effect may or may not happen. An implementation is allowed to perform an alternative evaluation that produces the same value without the side effect.

The pro-contracts paper P3846R0 defines contracts by three properties — they are redundant to program correctness, independently checkable, and their evaluation semantic is determined independently of the source code. That third property is the intended design. It is not a bug.

The Opposing Argument — One Inline Function in a Header

The title of the opposing paper isn't subtle: P3835R0, "Contracts make C++ less safe -- full stop", by John Spicer (EDG), Ville Voutilainen, José Daniel García Sánchez, September 3, 2025. The whole argument rests on one code example.

// z.h  — a third-party library header
inline void f(int x) {
  contract_assert(x > 0);
}

// l1.cpp — library implementation. Compiled with quick_enforce
#include "z.h"
void g() {
  f(0);   // expects the contract violation to be caught
}

// c1.cpp — client code. Compiled with ignore
#include "z.h"
extern void g();
int main() {
  f(1);
  g();
}

The library was built so that its own code is always checked with quick_enforce. The client builds with ignore. Because f is an inline function, only one definition survives under the ODR. So which semantic ends up actually applying after linking — whether the check the library author intended survives at all — depends on whether the compiler inlined the call inside l1.cpp, and which translation unit's copy the linker picked. In the paper's own words, there is no way for the programmer to know which semantic applies to which call.

This can happen wherever a header contains an inline function or template definition with a contract assertion in it — which is to say, in every header-only library. The paper states that modules don't solve this problem either. Its conclusion:

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

There's more in the same direction: P3829R0, "Contracts do not belong in the language" (David Chisnall, Spicer, Gabriel Dos Reis, Voutilainen, García), and P3573R0, "Contract concerns," which carries the names of Bjarne Stroustrup and David Vandevoorde. This was not a lightweight objection.

National body comments piled on too. The appendix to P3846R0, which tallies contract-related NB comments, counts 17 of them, from seven national bodies — Sweden, Spain, the US, France, Romania, Czechia, and Finland. Spain's ES-046 is summarized this way: critical checks can be removed, opening a security vulnerability that enables a new class of supply-chain attack. Several comments explicitly demanded removal from C++26.

If "supply-chain attack" sounds like an exaggeration, it's worth looking at the example P3829R0 gives. As P3846R0 summarizes it: a function is compiled with quick_enforce in one translation unit and ignore in another. When optimizing the quick_enforce side, GCC assumes the contract check is enforced and removes the caller-side null check. But at link time, the ignore version can end up being the one selected. Then there's no contract check and no null check that was removed on the assumption the contract check would catch it. Not one check gone — two. P3829R0 frames this as a design flaw in P2900 and argues that fixing it would require either (1) making mixed-mode an ODR violation, (2) a major change to compiler intermediate representations, or (3) giving up a core optimization.

There's a rebuttal to this from the pro-contracts side, and it's worth reporting fairly. P3846R0 states that EWG reviewed the mixed-mode concern at Hagenberg and declined to change the ODR for contracts, and that the GCC interprocedural-optimization problem in question was judged on the reflector to be "a compiler bug unrelated to contracts," and a reproduction case was reported to GCC. In other words, the pro-contracts side treats this as an implementation bug, not a language design flaw.

The Rebuttal — Functional Safety and Language Safety Are Different Things

The pro-contracts response is P3846R0, "C++26 Contract Assertions, Reasserted" (October 6, 2025). It's signed by 22 people led by Timur Doumler, Joshua Berne, and Gašper Ažman, and among the signatories are GCC's Jason Merrill and Jan Sando, libc++'s Louis Dionne, and Eric Fiselier, who implemented the Clang support. The paper answers 17 "concerns" one by one.

The core rebuttal starts by splitting a term in two.

  • Functional safety — the likelihood that a program performs its intended function without causing harm
  • Language safety — the absence of undefined behavior, or the ability to prove that absence

Contracts, the paper argues, are built for the former, not the latter. A checked assertion catches a bug; an ignored one documents intent with no runtime effect. The ability to configure the semantic externally isn't a flaw — it's a precondition for wide adoption.

Here's how the paper answers the cross-TU semantic mismatch from P3835R0's example.

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.

In other words, the worst case is a check going unchecked, and by design that cannot introduce a new bug into an existing program. The paper also states plainly that requiring a uniform semantic across the whole program would make it "a feature you cannot use at scale." A header's f might be used in a performance-critical loop, where nothing but ignore is affordable, while less-hardened caller code upstream might warrant quick_enforce — an assertion feature that can't support this variability, the argument goes, isn't useful.

The paper also notes that a proposal to change pre and post semantics so they're always checked was already voted on in Tokyo and rejected by strong consensus. Instead, it points to labels (P3400R1) as the path to expressing "must be checked," positioned as a post-C++26 extension.

Lay the two sides' claims side by side and the factual disagreement is actually narrow. Neither side disputes the behavior itself. Both agree checks can be silently dropped, and both agree there's no way to force it from the source. What they disagree on is whether that makes it "not a safety feature" or "actively unsafe."

What Got Fixed at Kona, and What Didn't Make It In

At the November 2025 Kona meeting, the committee spent Monday and Tuesday on contracts feedback. The outcome was this.

Keep contracts, but fix two bugs — reached by strong consensus. One of the adopted fixes was P3878R1, "Standard library hardening should not use the 'observe' semantic," authored by Voutilainen, Jonathan Wakely, Spicer, and Stephan T. Lavavej. This was a bug-fix paper written by people on the opposing side, and it was adopted — which is itself evidence that the opposition was technical, not political.

There's a control case worth noting. At the same meeting, trivial relocatability (P2786) had a decisive bug found that couldn't be fixed in time, and it was removed from C++26. So it isn't that the committee lacked the will to cut a feature — contracts survived on their own merits. In the same week, one feature got cut and the other kept.

And there's what didn't get in. At Kona, the committee agreed to pursue something close to the first solution in P3911R0, "[basic.contract.eval] Make Contracts Reliably Non-Ignorable," through the March meeting. In response to a Romanian NB comment (RO 2-056), the proposal was to add syntax to say, in the source code itself, "this assertion must be enforced." The proposed form looked like this.

int divide(int a, int b)
  pre! (b != 0);   // proposed syntax: always enforced. Did not make it into C++26

The paper reached R2 (January 14, 2026), and Andrei Alexandrescu and Herb Sutter joined as authors. But the current draft standard's grammar does not have this.

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

There is no pre! in the [dcl.contract.func] grammar, and the word "non-ignorable" appears nowhere in [basic.contract]. C++26's __cpp_contracts value stands unchanged from P2900R14's 202502L. Sutter's own Croydon report also states that "the feature was neither added nor removed."

To sum this up: C++26 gives you no way to write, in source code, that a given contract check must always run. The opposing side's core argument shipped unresolved with the standard, and that is the substance of the 12 dissenting votes.

One more gap worth flagging: C++26 contracts also cannot be attached to virtual functions. That capability, P3097R3 "Contracts for C++: virtual functions," landed in the C++29 draft instead (__cpp_contracts >= 202606L per the GCC status page), and GCC has not implemented it yet. For a feature whose whole pitch is documenting preconditions in an interface, leaving out virtual functions is a fairly large hole.

The Compiler Reality — One: GCC 16.1

This is the part that matters most in practice. On cppreference's C++26 compiler support table, the Contracts (P2900R14) row shows 16 in the GCC column, and the Clang, MSVC, Apple Clang, EDG eccp, and Intel C++ columns are all empty.

As of July 2026, GCC 16 is the only released compiler that implements C++26 contracts. GCC 16.1 shipped April 30, 2026, a month after the standard was finalized.

Here's how you use it.

# -std=c++26 alone does not turn contracts on. You need -fcontracts
g++ -std=c++26 -fcontracts main.cpp

# choosing an evaluation semantic (default is enforce)
g++ -std=c++26 -fcontracts -fcontract-evaluation-semantic=quick_enforce main.cpp

Cross-referencing the GCC 16 manual with the option definitions in the source gives this.

FlagValuesDefault
-fcontractsenables the contracts featureoff
-fcontract-evaluation-semantic=ignore / observe / enforce / quick_enforceenforce
-fcontracts-client-check=none / pre / all — insert caller-side checksnone
-fcontracts-definition-check=on / off — callee-side checkson
-fcontracts-conservative-iparestricts interprocedural optimizationon
-fcontract-checks-outlinedsplits checks into a separate functionoff

One small trap: the manual text writes the values as ignored and observed, but the strings the compiler actually accepts are ignore, observe, enforce, and quick_enforce (per the enum definition in GCC's source, gcc/c-family/c.opt, default Init(3) = enforce). Copy-paste from the manual and you'll get an "unrecognized contract evaluation semantic" error.

You override the violation handler by defining this function.

#include <contracts>

void handle_contract_violation(const std::contracts::contract_violation& v) {
  // logging, etc.
}

The flag worth paying attention to in that table is -fcontracts-conservative-ipa. In GCC's own documented words, this flag "prevents interprocedural analysis from taking action when the body of an inline function is visible in some translation unit, since the conditions for the evaluation of contracts might vary between translation units and such action could be incorrect." And it's on by default.

This is exactly the null-check-elision scenario from P3829R0 above, made concrete. The property the opposing side flagged — that semantics can differ across TUs, so optimizing on the assumption they don't is dangerous — is embodied in GCC as a flag that conservatively restricts interprocedural optimization by default. The same idea shows up in the description of -fcontract-disable-optimized-checks, which talks about "optimizations that could unintentionally elide a check step."

Which side's evidence this counts as depends on how you read it. The opposing side reads it as "the language is offloading this burden onto the compiler." The pro-contracts side reads it as "this is a manageable implementation problem, and it's being managed." What's not in dispute is that this isn't an abstract philosophical debate — it's a thing that exists as a compiler flag turned on by default.

For what it's worth, the same GCC 16 also implements reflection (P2996R13), which needs its own separate -freflection flag. And P2795R5 (erroneous behavior), which removes the undefined behavior of reading an uninitialized value, also landed in GCC 16 — unlike contracts, that one was uncontroversial, and you get the benefit just by recompiling.

So, When Should You Use It — and When Shouldn't You

Start with when not to.

  • Do not use it to validate a trust boundary. This is the most important one. User input, data coming off the network, permission checks — none of that belongs in a contract. Build with ignore and it vanishes; if it's in an inline function in a header, it can vanish regardless of your intent. Validate trust boundaries with if and explicit error handling. Even P3846 states that "a mechanism for non-ignorable checks already exists — for instance, if statements." That's not the opposing side talking. That's a sentence from the pro-contracts paper.
  • Do not use a predicate with side effects. Whether it evaluates at all is unspecified.
  • Do not use it in code that needs to be portable. As of July 2026, this is a GCC-16-only feature. Neither Clang nor MSVC has it in a release.
  • You can't use it on virtual function interfaces yet. That has to wait for C++29.
  • Inside a predicate, outer variables become const and this becomes a const pointer (const-ification). A check that calls a non-const operation simply won't compile.

Where it earns its keep.

  • Documenting preconditions in the interface in an internal codebase. Being able to see the contract just by reading the header is something assert never gave you, and that benefit survives even a build with ignore.
  • The classic pattern of enforce in debug/test builds and ignore in release. This is doing what assert already did, with better syntax, and this is realistically where most early adoption will land.
  • In an internal service where the build is locked to a single compiler (GCC), compiling every TU with the same semantic. This is exactly what P3846 calls the "natural initial strategy" — a single semantic per TU. If you never mix modes, P3835R0's problem never arises in the first place.

In other words, contracts work fine when you control the entire build. The trouble starts the moment you pull in a third-party header-only library, and the moment a library author wants to say "my check must always be on." C++26 gives them no syntax to write that down.

Closing

To sum up: C++26 contracts survived the final vote on March 28, 2026, at 114 in favor, 12 against, 3 abstentions — the endpoint of 20 years of discussion, 5 years of work by SG21, and one prior removal back in C++20. The feature itself is real: preconditions and postconditions attached to a declaration do something assert never could, and the four evaluation semantics reflect a genuinely wide range of real-world needs.

At the same time, the opposing argument is also real. Source code cannot determine whether a check runs, and inside an inline function in a header, a programmer cannot know which semantic will actually apply. This is not a bug — it's the intended design — which is exactly why it's not the kind of thing that gets "fixed." The syntax for forcing a check from source (P3911) was pursued through March but did not make it into C++26, and labels (P3400) remain a post-C++26 item.

So the right posture toward this feature is clear. Contracts are a bug-detection tool, not a safety feature. Even the pro-contracts side says as much — built for functional safety, not language safety. Miss that distinction, read it as "C++26 makes things safe via contracts," and you'll get hurt exactly the way the opposing side warned: by putting a check that can be silently skipped on a trust boundary.

And as of July 2026, if you want to try it at all, GCC 16.1 is your only option. Sutter expects C++26 adoption to move fast, but on contracts specifically, the only thing you can do right now is experiment with GCC 16. Putting this into portable production code is a conversation for after Clang and MSVC catch up.

References