마음에 들지 않음. 런타임 오류를 컴파일 시점 오류로 바꿔 준다면 계약을 기꺼이 쓰겠지만, 이 기능은 런타임 오류만 늘리는 듯함. 검사를 정말 수행하겠다는 약속도 확실하지 않다면 assert보다 나을 게 없어 보임.
글에서도 계약 단언은 오류 처리나 입력 검증에 쓰면 안 되며, 함수가 특정 사건을 오류로 보고해야 한다면 생략될 수 없는 일반 C++ 제어문으로 처리해야 한다고 강조함. 입력 검증은 아니고 매개변수 검증이지만, 검증도 아니고 오류를 내는 검사인데 오류를 만드는 것도 아니라니, 용도가 이렇게 모호하면 혼란이 생길 수밖에 없음.
가장 큰 문제는 이 검사를 반드시 실행하라고 지정할 방법이 없다는 것임. 계약 기반 프로그래밍 자체가 아니라 C++의 계약 구현이 이상하다는 비판임.
작성자들과 명세가 제시하는 모델에서 계약은 실행을 전제할 수 없는 ‘유령 코드’임. 컴파일러에 검사를 실행하도록 설정해도 ABI/ODR 문제로 검사가 제거될 수 있고, observe 모드도 있어서 실행된다고 가정하면 안 됨.
예를 들어 assert(p != nullptr); assert(p->some_value());를 contract_assert(p != nullptr); contract_assert(p->some_value());로 무조건 바꾸는 것은 계약 기능의 오용임. 기존 단언으로는 동작이 잘 정의되던 코드가 정의되지 않은 동작을 일으킬 수 있음. 안전 검사로도 믿고 쓸 수 없다면 도대체 용도가 무엇인지 모르겠고, 10년 뒤에는 안전성이 필요한 코드에서 계약을 쓰지 말라는 지침이 나올 것 같음.
사전 조건과 사후 조건을 표현하는 표준적인 방식이며, Clojure도 이렇게 동작함. 이를 모두 컴파일 시점 검사로 강제하면 언어에 정제 타입(refinement types)을 갖춘 두 번째 타입 시스템을 도입하는 셈이고, 타입 검사도 오래 걸리게 됨. 정수 인자 x의 사전 조건이 collatz(x)==1이면 컴파일러가 콜라츠 추측을 증명하거나 컴파일할 때마다 모든 64비트 정수를 검증해야 할까?
Clojure에서 써 보니 일반 단언보다 작지만 의미 있는 개선이었고 매우 마음에 들었음. 검사를 끌 수 있다는 것도 결함이 아니라 기능임. LiquidHaskell 같은 것을 기대하면 안 됨.
입력 검증에 계약을 쓰면 안 된다는 구분은 단언과 다르지 않음. 단언이 부적절한 곳에는 계약도 부적절하다는 뜻이므로, 경계가 모호하다면 계약 이전부터 그랬던 것임. 단언이나 계약의 실패는 버그를 뜻함. 절대 발생해서는 안 되는 일이므로 검사를 생략해도 괜찮아야 하고, 실패하면 코드를 고쳐야 함. 호출자가 제약을 이미 충족했다고 가정하는 게 아니라 함수가 직접 강제하기로 약속했다면, 그 검증은 런타임 코드에 넣어야 함.
일어날 수 있지만 일어나서는 안 되는 일, 예를 들어 사용자가 빈 이름을 입력하는 것은 입력 검증의 대상임. 호출 함수가 문자열이 비어 있지 않음을 이미 확인했거나 생성자가 연결 리스트의 머리와 꼬리를 만들었듯이, 일어날 수 없고 일어나서도 안 되는 일은 계약의 대상임.
버그 대부분은 프로그래머가 일어나지 않을 것이라 생각한 일이 실제로 벌어지는 데서 생기므로 계약이 유용함. 버그 없는 프로그램에서는 아무 역할도 하지 않으니 컴파일러 옵션으로 끌 수 있는 것도 합리적임. 단언과 비슷하지만 도구와 IDE에서 더 유용하고, observe 모드로 프로세스를 종료하지 않고 운영 환경의 버그를 찾을 수도 있음.
계약은 호어 논리의 사전 조건·사후 조건·불변식을 명시하기 위한 기능임. 컴파일러가 정적으로 검증하면 실행할 것이 없어지고, 그렇지 않으면 런타임에 검사하거나 비용을 피하려고 아예 비활성화할 수 있음.
도구가 발전하면 점차 정적·컴파일 시점 단언으로 발전할 것임. 입력 검증이 아니라, 코드 내부에서 기대하는 조건을 주석 대신 코드로 표현하는 용도임.
왜 유용하지 않았는지 더 자세히 알려 줄 수 있을까? 제대로 구현하면 여러 경우에 컴파일 시점 경고·오류를 제공하고 릴리스 빌드의 최적화기에도 추가 정보를 줄 수 있을 것 같음. 전체 프로그램을 한꺼번에 빌드하면 함수 구현 안의 일반 단언으로도 가능하겠지만, 함수 원형에 있으면 실용적으로 더 쉬워질 듯함.
글에 최적화 용도는 나오지 않음. if (!(some_condition)) { __builtin_unreachable(); } 같은 관용구처럼 최적화 단계에 정보를 전달하는 것도 목적에 포함될까?
선택한 평가 의미론에 따라 확실성의 정도는 다르지만, 사전·사후 조건이 GCC와 LLVM의 최적화 패스로 전달된다고 알고 있음. 다만 효과나 신뢰성을 자세히 다룬 글은 보지 못했고, 이전에 읽은 자료로 아는 범위는 여기까지임.
주된 목표는 아님. assume 의미론을 논의하기는 했지만, 표준화된 계약 평가 의미론에는 포함되지 않음.
최적화에 영향을 주려는 기능이라는 인상은 받지 못했음.
나만 그런지 모르겠지만, C++처럼 거대하고 복잡한 언어는 여전히 매력이 없음. 직접 코드를 쓸 거라면 아름답고 쓰기 즐거운 언어가 어느 때보다 더 필요함.
Rust도 상당히 복잡하지만 인기가 많음. Python 역시 크고 복잡하되 복잡성을 점진적으로 드러내기 때문에 언어의 많은 부분을 몰라도 일을 할 수 있음.
단순한 언어가 좋다는 데는 동의하며, 나는 Go를 매우 좋아함. Python의 선에 나오는 ‘일을 하는 방법은 하나여야 한다’는 철학은 정작 Python보다 Go에 더 잘 들어맞음.
많은 프로그래머가 작업에 맞는 언어를 고르지만, C++의 철학은 보기 흉한 부분도 기능으로 받아들이는 데 있는 듯함. 사용할 수 있는 언어의 부분집합이 워낙 다양해서 다른 언어로 떠날 필요가 없음.
C++ 애호가를 포함해 거의 모두가 언어가 너무 크다는 데 동의함. 다만 계약은 이미 존재하는 유용한 개념을 공식화하는 기능임.
C++를, 점점은 C#과 Java도 언어 기능의 실험장으로 봄. 가능한 모든 기능을 넣어 무엇이 괜찮고 무엇이 아닌지 확인하면, 다른 언어는 그중 필요한 것만 골라 더 명확한 철학과 단순한 설계를 갖출 수 있음.
계약이 앞으로 어떻게 발전할지 기대됨. 자동 증명 도구가 사소한 경우에라도 코드의 정확성을 증명할 수 있을까? 단위 테스트에서는 어떻게 처리해야 할까? 범위를 벗어난 값의 처리를 시험하고 싶을 때도 있고, 검사 비용이 커서 런타임 검사를 피하려고 계약을 넣을 때도 있음. 최적화기가 계약을 활용할 수 있고, 활용해야 하는지도 궁금함.
Nguyễn 등의 Soft Contract Verification, Soft Contract Verification for Higher-Order Stateful Programs 논문을 참고하면 좋음. Racket 커뮤니티에서 관련 연구가 많이 진행됨.
일부 종류의 계약은 정적으로 증명할 수 있지만 전부는 아니며, 언어 의미론에 따라 달라져 C++에 뒤늦게 적용하기는 쉽지 않을 듯함. 타입과 계약을 통합해 정적 증명과 동적 검사를 넘나드는 연구도 진행 중임. 정적으로 증명하면 런타임 비용을 없앨 수 있고, 타입으로부터 계약을 생성하면 점진적 타입 언어의 건전성을 확보할 수 있음.
단순한 불리언 비교가 아니라면 유용한 오류 메시지를 출력하는 사용자 정의 단언 함수를 선호함. Assertion failed: index < container.size()만으로는 실제 비교한 값이 무엇인지 알 수 없음.
C에서는 void AssertWF(bool assertResult, char *format, ...) 같은 함수를 사용함. 호출 예는 AssertF(index < container.size(), "index:%d < container.size():%d", index, container.size());임. 첫 인자가 거짓이면 fputs와 vfwprintf로 상세 메시지를 stderr에 출력한 뒤 abort()를 호출함.
계약을 어디에 넣어야 할까? 헤더에 넣고 Foo foo의 foo.size() > 3 같은 조건을 검사하려면 전방 선언만으로는 부족해서 온갖 헤더를 포함해야 함. 헤더를 가볍게 유지해야 컴파일이 한 시간씩 걸리는 일을 피할 수 있으니 받아들이기 어려움. cpp 파일에 넣으면 헤더에서 문서 역할도 한다는, 단언 대비 유일한 장점이 사라짐. 조금 더 화려한 단언을 위해 표준에 50쪽을 추가한 것일까? contract_assert(expr)라는 내장 기능 하나에, 식이 거짓이면 정의되지 않은 동작이며 디버그 빌드에서는 런타임 검사를, 실패를 정적으로 알아낼 수 있으면 컴파일 오류를 내도록 권장한다는 주석이면 충분했을 것 같음.
3년쯤 뒤에는 작지만 의미 있는 코드베이스에 계약을 도입하려고 많은 노력을 기울인 개발자가 강연과 책, 블로그로 이런 질문에 답해 줄 것 같음.
올바른 위치는 헤더라고 생각하며, 긴 컴파일 시간을 피하는 방법도 해결해야 함. 모듈이 해법의 큰 부분일 듯해서 실제 환경에서 모듈을 쓰는 방법도 함께 다뤄야 할 것임. 물론 모두 추측이라 틀릴 수도 있음.
엄밀하게는 함수의 첫 선언에 사전·사후 조건을 넣어야 함. 이후 재선언에는 계약 조건을 생략하거나 최초 선언과 같은 조건을 넣어야 함.
조건을 외부에서 볼 수 있는 동작으로 표현하거나, 내부 동작을 검사하는 별도 함수를 호출하면 모든 헤더를 다시 포함할 필요는 없음. 최초 선언 위치에서 접근 가능한 것으로 조건을 정의하고, 더 복잡한 검사는 필요한 내부 정보에 접근할 수 있는 다른 위치의 함수에 맡기면 됨.
C++26용 기능이니 모듈을 쓰면 됨.
설계할 때 어떤 용도를 염두에 뒀는지 궁금함. 런타임 단언과 문서화에는 쓸 수 있겠지만, 계약 없이도 그럭저럭 가능함. 형식 검증을 위한 것이라면 설계가 크게 빗나가 있음.