January 13, 2025·4 min read·engineeringcppcontractssafetystandards

The C++ contracts debate needs a precise meaning of safety

P3500 separates correctness assertions from language safety guarantees. Its examples show why enforcement and valid predicate evaluation are independent requirements.

A bounds assertion can fail to protect an access for two different reasons. The build can choose not to enforce the assertion, or evaluating the assertion can itself have undefined behavior. Those cases require different explanations. P3500R0 makes them the starting point for a debate about what the proposed C++ contracts facility should guarantee.

Timur Doumler, Gašper Ažman, and Joshua Berne published Are Contracts safe? on January 13. Their paper defends the contracts design proposed for C++26 while acknowledging those concerns. It argues for treating correctness assertions and language safety guarantees as complementary work. That is the authors' position, not a conclusion that every participant in the debate shares.

The underlying P2900R13 proposal, also dated January 13, defines preconditions and postconditions as well as assertion statements. It supplies four evaluation semantics and a user-defined violation handler. As of these papers, the feature is a proposal for the upcoming standard, not an adopted language guarantee.

Enforcement is a deployment choice in the design

The four proposed semantics are ignore, observe, enforce, and quick-enforce. Ignore does not check the predicate. Observe checks but permits continuation after handling a violation. The enforcing choices prevent execution from continuing past a detected violation, with different handling paths. That flexibility is a deliberate part of the proposal.

P3500 motivates it with contexts where terminating the process is unacceptable and contexts where checking costs cannot be paid in every deployment. It also discusses introducing assertions into existing systems whose specifications may initially be too strict. Observation allows those assertions to provide information without immediately making every discovered violation a process termination.

The cost is that a function declaration alone does not provide an unconditional promise that execution stops on a false precondition. Generic library code does not necessarily control the build policy of every application that uses it. This is the tension between a reusable assertion facility and a check that an implementation must always enforce.

I value RAII as C++'s strongest practical argument because it gives resource cleanup a language-defined relationship to scope exit. Contracts raise a different question about guarantees. A visible precondition is useful documentation, but the selected evaluation semantics still determine what happens when a caller violates it.

The predicate remains a C++ expression

P3500's second concern survives even with enforcing semantics. A contract predicate follows ordinary C++ expression rules. If it evaluates an invalid operation, writing it inside an assertion does not repair that operation. The facility does not supply a separate universally safe language for evaluating its predicates.

The paper illustrates this with a helper that adds 100 to a signed integer. A proposed precondition compares that result with the original argument. The following helper expresses the problematic computation without using proposed contract syntax.

int shifted(int value)
{
    return value + 100;
}

For sufficiently large value, the addition has signed-overflow undefined behavior. Using shifted(value) > value as a supposed overflow check is therefore unsound. As P3500 explains, the compiler can reason that defined evaluation never overflows and remove a comparison based on that assumption. Selecting an enforcing contract semantic does not make the helper a valid overflow detector.

This separates two review questions. One concerns what happens if a well-defined predicate evaluates to false. The other concerns whether the predicate can be evaluated with defined behavior for the inputs it is supposed to examine. Treating both as simply asking whether contracts are safe obscures the mechanism that needs attention.

Correctness assertions express application requirements

The authors distinguish language safety from functional safety and security. Their central claim is that contracts let programmers state parts of a program's requirements that the compiler cannot infer from the language alone. A check can detect a logical violation even when every individual memory access follows the language rules.

For example, a value can be in range for its type while being outside the range that a particular algorithm accepts. A type-level guarantee that the value is an integer does not establish that application-specific property. A contract predicate can express the additional requirement when the programmer knows what it is.

That is a persuasive use for assertions without making them a substitute for removing undefined behavior. The two activities operate on different requirements. One defines what an application intends, while another constrains what operations the language permits. P3500 argues that delaying the first until the second is solved would withhold useful checking from existing code.

The paper's broader claims about safety outcomes remain arguments for the proposed design. A contracts facility only checks the requirements actually expressed, under the semantics actually selected, using predicates whose evaluation is valid. Those conditions describe its operation more precisely than an unqualified promise that adding assertions makes a program secure. They also explain why a violation report is evidence of a failed stated requirement, while the absence of reports cannot establish that every requirement was expressed or checked during execution.

Stronger controls remain a separate design problem

P3500 discusses labels that could constrain evaluation semantics for individual assertions. An always-terminating assertion is one use case, alongside assertions that must not terminate or that exist only for static analysis. The authors prefer a more general control facility and argue that it should not block the initial proposal.

It also discusses strict predicates that would exclude undefined behavior during checking. The difficult case is accessing objects through pointers or calling existing member functions. The paper argues that a practical subset with strong guarantees is much harder to specify than a separate arithmetic-only expression language.

I remain interested in the distinction between a guarantee and a configurable diagnostic. C++ already asks readers to understand several generations of language and library rules. A contracts design should make the status of a check clear enough that a programmer knows whether it is documentation, an observed assertion, or an enforced boundary. January's papers make those choices explicit enough to debate, while leaving the standardization decision ahead of them.