Class invariants make contract checking a synchronization question
P3361R0 argues for flexible contract handling and examines class invariants. Its concurrency examples show why a condition over object state needs a defined synchronization boundary.
A condition over two fields is meaningful only if the fields belong to a coherent view of the object. Reading each field safely is insufficient when another operation can change the relationship between the reads. Esa Pulkkinen's P3361R0, dated 18 July, brings that problem into the contracts discussion through class invariants. The paper is a discussion of design philosophy and possible mechanisms, with no claim that its invariant syntax is an adopted C++ facility.
RAII is C++'s strongest feature for me because it gives resource lifetime a local structure. A lock guard is an example of that benefit. Contract checking over shared state needs to respect the same protected region, because a check is itself an operation that reads the object.
An invariant concerns a relationship over state
Pulkkinen treats class invariants as constraints on members and discusses checking them around operations. His concurrency examples include a relationship between two fields. Loading two atomic variables independently does not guarantee that their values describe one indivisible object state. A condition can therefore observe a mixture even though the individual loads are permitted.
The engineering consequence is that a predicate needs an execution context as well as a boolean expression. A reviewer must know when the state can change and what excludes those changes during evaluation. A declaration saying that an object maintains a relation cannot itself supply the synchronization required to read that relation.
This applies to diagnostic code as much as production computation. A check that reads shared state is participating in the program's memory accesses. Moving the same expression into an annotation does not make interference disappear. The compiler and library design need to determine where that expression executes relative to the synchronization protecting the data.
Lock placement changes what a check can observe
The paper discusses placing invariant checks inside the member's locked region. Checking before the member acquires the lock can inspect state without the intended exclusion. Acquiring the same non-recursive lock inside a predicate that the already-locked member calls creates another problem. The predicate's locking contract needs to be explicit.
One of the paper's examples passes a unique_lock to the checking function and describes a precondition about ownership of the relevant mutex. The intended point is that a check can itself have a prerequisite. That prerequisite belongs to the operation's design, alongside the relation it evaluates.
My view is that this is the useful part of the invariant discussion. It identifies information a checking interface needs, instead of assuming that a predicate can be evaluated at any convenient boundary. The same expression can be appropriate inside a protected region and inappropriate immediately before it.
For a library author, a public invariant query therefore needs a clear answer about who obtains synchronization. An internal helper can require that its caller already holds it. A public operation can obtain it itself. The choice changes how those functions compose and whether a nested call is permitted.
Violation handling is a separate policy dispute
P3361R0 also argues that libraries should not terminate the entire process when a contract check fails. It favors exception-based handling and gives application-level control a central role. These are the author's positions in the ongoing design discussion. They should not be reported as committee consensus or existing contracts semantics.
The disagreement concerns the authority of a component. A library knows a local requirement has failed. The application knows which wider operation it is performing and what recovery it permits. Transferring control to the application can preserve that separation, but the application still needs valid state from which to recover.
An invariant violation makes that latter condition particularly important. If an operation has already broken the object's relation, unwinding does not automatically repair it. Recovery needs a defined state transition, such as abandoning the object or restoring a valid representation. Merely selecting exceptions as the reporting mechanism does not supply that transition.
Interface contracts have another source of context
The earlier P3097R0 separates assertions on a statically selected interface from those on a dynamically selected implementation. That is a different problem, but it shares a useful principle. The place where a condition is written is not sufficient to determine all the conditions under which it should run.
For a class invariant, the relevant context includes the object's synchronization and lifetime state. For a virtual call, it includes the interface used by the caller and the override selected by dispatch. A general contracts mechanism must fit these existing execution relationships if its checks are to provide reliable information.
The practical contribution of P3361R0 is to make those requirements visible in the debate. A proposed invariant needs more than a predicate over members. Its evaluation must occur where the members can be read coherently, and its failure policy must agree with the object's recovery contract. Those details determine whether a check describes a stable object property or merely samples state at an unsuitable moment.