January 11, 2024·4 min read·engineeringcppcontractsconstexpr

Compile-time contracts raise a different failure-policy question

P3079R0 questions whether constant evaluation needs ignore and observe semantics. The disagreement concerns build cost and adoption, alongside what it means to continue after a known contract violation.

A program already serving requests and a program still being compiled have different reasons to continue after discovering a violated assumption. Oliver Rosten's P3079R0, listed on 11 January, asks whether the proposed C++ contracts facility needs ignore and observe semantics during constant evaluation. The question is part of an unfinished design discussion. It does not describe a facility already adopted into the language.

C++ has useful mechanisms for expressing obligations, but the cost of reading several overlapping language rules is one of my reservations about it. A contracts design should make the failure policy understandable at the point where a check is used. Constant evaluation adds another context in which that policy needs a precise meaning.

The earlier proposal seeks regularity

Timur Doumler's P2894R1 proposes implementation-selected contract semantics during constant evaluation, analogous to the runtime choices. An enforced failed check would prevent successful compilation. An observed violation could produce a diagnostic while permitting compilation to continue. Ignoring would avoid evaluation of the check, although the predicate still needs valid source syntax.

The motivation includes compile-time cost and incremental adoption. A library can add annotations to existing code without requiring every consumer immediately to use the strictest policy. The proposal also considers a compiler choosing different semantics for runtime execution and constant evaluation. These are proposed capabilities, with their own consistency requirements.

The tradeoff becomes clearer when the check itself contains a defect. Continuing after a faulty check might let consumers use otherwise correct code. It might also hide a real caller error if the consumer wrongly decides that the annotation is at fault. The choice changes what evidence a successful build supplies about the calls it evaluated.

Rosten asks for justification beyond symmetry

P3079R0 begins with a thought experiment in which constant evaluation costs nothing. It asks which remaining reasons justify ignoring a check, then returns to whether real compilation cost is sufficient. Rosten discusses non-constant predicates and immature annotations, and argues that the case for compile-time ignore and observe needs more evidence.

This challenges the idea that the runtime policy should automatically determine the compile-time policy. Runtime continuation can address an immediate operational requirement. Compilation happens before that particular execution begins. A build failure can still be disruptive, but its consequences differ from terminating a deployed process in the middle of work.

My view is that this difference deserves an explicit design argument. Uniform vocabulary can reduce learning cost, while different contexts can still need different guarantees. The relevant question is what the policy enables for a library author and what its consumer can conclude afterward. A shared name alone cannot establish that equivalence.

A runtime predicate needs an explicit compile-time interpretation

Both papers discuss a precondition that depends on runtime hardware availability. Such a condition cannot simply be evaluated as an ordinary compile-time query. The example forces the API author to decide what the precondition means when the operation is constant-evaluated.

One possible design gives the predicate a separate constant-evaluation branch. That expresses the intended condition directly. Another is to skip the predicate under the proposed ignore semantic. The latter can enable the call without explaining why the hardware-dependent requirement is irrelevant at compile time.

This is a substantive interface question. If the operation has a valid compile-time interpretation, its contract should describe that interpretation. If it fundamentally requires runtime state, constant evaluation should not acquire validity merely because a check was disabled. The design needs to distinguish the predicate's implementation limitations from the operation's actual domain.

There is also a difference between a predicate that cannot be a constant expression and one that could be made evaluable with additional implementation work. Rosten explicitly separates those cases. That distinction affects whether ignore addresses an unavoidable mismatch or avoids completing a predicate that should support the function's advertised use.

Compilation cost is a separate argument

A check that is inexpensive during one runtime call can still consume substantial work when evaluated many times during compilation. Conversely, a general concern about C++ compile times does not establish the cost of a particular class of contract checks. Rosten's slides leave the prevalence of expensive compile-time predicates as an evidence question.

That is a useful boundary for the debate. The semantics should account for genuine implementation and adoption costs, but those costs need a concrete description. A compiler option that changes checking policy also changes the interpretation of a successful build. Library authors need to know which guarantee they can rely on across supported configurations.

The immediate result of P3079R0 is a sharper design question. WG21 must decide whether the benefits of compile-time continuation justify multiple checking outcomes, and how those outcomes interact with predicates that cannot be evaluated. The answer will determine what a contract annotation promises to a caller whose invocation happens during translation.