Several concerns have been raised about the C++26 contracts facility, P2900, most compactly in P3173R0, "P2900R6 May Be Minimal, but It Is Not Viable": that it doesn't prioritize safety, that the evaluation of a contract assertion can have undefined behavior, that it doesn't support virtual functions and function pointers, and that it isn't applied to the standard library. A further concern, raised separately, is that P2900 leaves too much implementation-defined. This paper is an overview: for each concern, which paper addresses it, and how. Each of those papers is backed by an implementation, in a GCC branch that can be tried on Compiler Explorer.
operator() decides at run time what an evaluation
does.| Concern | Addressed by | How |
|---|---|---|
| Safety isn't prioritized (P3173R0, section 2) | P4423R0; P4425R0 | Safety is a property a contract can have, chosen per contract, and verified: a strict contract's evaluation is proven free of undefined behavior; a symbolic one has nothing to evaluate. Ordinary P2900 contracts don't change. |
| Undefined behavior in a contract's evaluation (P3173R0, section 3) | P4423R0 | A strict predicate, and every function it calls, is checked
against mandatory rules: its references and this are
object addresses, it changes nothing, its arithmetic can't overflow,
it establishes its callees' preconditions. Violating one is an
error. |
| Dynamic dispatch (P3173R0, section 4) | P4427R0 | A control object's inherited() decides whether an
overrider inherits the contract, and
base_contract<Base>() lets an override's own
contract weaken or strengthen its base's, so the model in the working
draft, wholesale inheritance and the Design-by-Contract model can all
be expressed. |
| Indirect calls (P3173R0, section 4) | P4426R0 | A contract on a function pointer declaration is checked on every call through it, with no change to the pointer's representation or to the ABI. |
| No application to the standard library (P3173R0, section 5) | P4324; P4423R0 | The library's hardened preconditions are implemented with P4324 contracts, in non-strict form; as strict contracts, 478 of its 509 hardened assertions, in 204 functions across 44 headers, with 60 declared preconditions. |
| Too much implementation-defined behavior | P4324 | What P2900 leaves to the implementation, a control object does, in portable code: choosing the semantic at run time, repeating an evaluation, choosing where the check is made, what a violation does. |
P3173R0 recalls SG21's 2020 poll to "progress contract checking to enforce software safety first", and finds that P2900 doesn't deliver on it. P4423R0 does, without changing what P2900 specifies: a contract becomes a strict contract by naming a control object that says so, and for a strict contract the compiler verifies, rather than trusts, that evaluating it is safe. In November 2024, EWG polled against making the MVP itself stricter; the strict contracts of P4423R0 are additive, so they don't ask EWG to revisit that.
Symbolic contracts go further for the contracts they're used for: a symbolic predicate has no body, so it has no side effects and no undefined behavior by construction, and its truth is established by the compiler from the postconditions of earlier calls.
P3173R0's central objection is that a check meant to guard against
bugs can itself be elided, or exploited, through undefined behavior in
its own evaluation; it refers to
P2680 for the remedy. P4423R0
implements that remedy. A strict predicate, a conveyor
predicate, may call only conveyor functions, and both are
checked at compile time against rules that rule out undefined
behavior: every reference and this is the address of an
object, nothing is modified, arithmetic doesn't overflow and doesn't
divide by zero, every call establishes its callee's preconditions. A
violation is an error, not a warning.
A strict contract is not limited to toy predicates: the standard library's own assertions, listed below, are expressed that way.
For virtual functions, the debate has been between a
Design-by-Contract model, where an overrider's contract relates to the
overridden function's, and the model in the working draft, where it
doesn't. Each has been called unsuitable by someone. "Configurable
contracts for virtual functions" doesn't pick one. A control object
declares inherited(), and an override that declares no
contract of its own inherits the contract when it says so, separately
for the caller's side and the definition's. An override's own contract
can refer to its base's with base_contract<Base>():
base_contract<Base>() || extra is a weaker
precondition, base_contract<Base>() && extra a
stronger postcondition, which is Design-by-Contract subcontracting. The
model in the working draft, wholesale inheritance and Design by Contract
can then each be expressed, contract by contract.
For function pointers, "Contracts for (member) function pointers" attaches a contract to a function pointer's declaration and checks it on a call through that pointer:
void f(int a, int b); void (*p)(int a, int b) pre(a <= b) = f;
Nothing is carried along when the pointer is copied, so neither the pointer's representation nor the ABI changes; a copy declared with a different contract, or none, can get a quality-of-implementation warning.
P3173R0 asks for the facility to be applied to the standard library. In the implementation, P4324 contracts, in non-strict form, implement the library's hardened preconditions: each is a contract assertion whose control object enforces it. The same assertions can also be strict contracts: 478 of the library's 509 are, in 204 functions across 44 headers, with 60 declared preconditions; the remaining 31 are assertions whose predicates can't be strict. The library's saturation arithmetic functions are conveyor functions, so strict predicates can use them. Details are in P4423R0.
Another concern raised about P2900 is that it leaves too much implementation-defined: which evaluation semantic applies, usually chosen by a compiler flag for the whole program; whether an assertion is evaluated more than once; whether the check is made on the caller's side or the callee's; what a violation does beyond calling the violation handler. Each is a door left open for an implementation to go through.
P4324 doesn't leave those doors open. It lets the program go through
them, portably, in the control object it names. A control object's
type answers the compile-time questions (whether the assertion is
ignored, whether it is constified, on which side it is checked), and
its operator(), handed an assertion_context,
decides what an evaluation does: whether to evaluate the predicate, how
many times, and what to do when it fails. So what P2900 makes
implementation-defined is, under P4324, just code.
Semantics chosen at run time are a control object whose
operator() consults the program's own state to decide
whether, and how, to evaluate the predicate; P4324 gives an example.
Repeated evaluation is a loop in a control object's
operator(), calling ctx.check() as many times
as it chooses. The number of evaluations can be a compile-time
constant,
https://godbolt.org/z/Kv6xd7Ka1,
or a run-time variable, so the program decides while it runs,
https://godbolt.org/z/vjodhxP5n.
These are ordinary user code, and behave the same with every implementation of P4324. None of them needs a compiler flag, and they can be used side by side in the same program, each by the contracts that name it.
All of the above is implemented in a GCC branch, and can be tried on
Compiler Explorer with the "x86-64 gcc (P4324 contracts, and profiles)"
compiler, using -std=c++26 -fcontracts
-fcontract-control-objects. The papers listed above each give
their own examples.