short

C++26 contracts, and the const rule the compiler makes you learn

· english· audience: working-cpp· AI-generated, reviewed by Filip Sajdak

C++26 shipped four headline features: reflection, std::execution, std::simd and contracts. Contracts got the least attention of the four, partly because the design was contested right up to the vote, and partly because until recently there was nothing to try. That has changed. GCC 16.1 implements P2900, and you can write preconditions today.

They are also still being argued about, which is worth knowing before you read the rest. The August 2026 mailing carries P4334R0, “P2900 Contracts’ fundamental flaws”, by Bjarne Stroustrup, J-Daniel Garcia, Vinnie Falco, John Spicer and Ville Voutilainen, which calls the design “an existential threat to C++” and asks for it to be replaced with something smaller before C++26’s final ballot. Several companion papers argue the same case, and the authors of P2900 have answered them in turn. Nothing below is affected: the feature is in the working draft, GCC implements it, and the code in this post compiles and runs. But a reader deciding whether to build on contracts should know the design is under active challenge rather than settled.

The syntax attaches to the declaration rather than the body:

10 / 2 = 5
7 / 1  = 7
shrink(9) = 4
all contracts held

Three pieces: pre for what the caller must guarantee, post for what the function promises (with r: naming the result), and contract_assert for a check inside the body. No header is required, because contracts are core language rather than library, though GCC still needs -fcontracts to enable them.

The const rule

Here is the thing that catches you on first contact. Write this:

int divide(int a, int b)
    post (r: b == 1 ? r == a : true)   // error

and the compiler refuses:

error: a value parameter used in a postcondition must be const

The reason is sound once stated. A postcondition is evaluated after the body has run, and the body is free to reassign a by-value parameter. If a could have changed, then r == a is checking the result against whatever a happens to hold at the end, which is not the promise you meant to make. Rather than let you write a postcondition that silently means something else, the standard requires the parameter to be const so that it demonstrably has not changed.

Add the const and it compiles. The rule only applies to parameters actually named in a postcondition: the shrink function in the demo takes a non-const int and mutates it happily, because it only has a precondition.

What a violation does is not in your source

The second surprise is that the same code behaves four different ways depending on a compiler flag. The evaluation semantic decides what happens when a check fails:

  • ignore does not evaluate the check at all.
  • observe reports the violation and continues.
  • enforce reports and terminates. This is the default.
  • quick_enforce terminates without reporting anything.

That last one is easy to miss and the spelling is unforgiving: GCC takes quick_enforce and rejects quick-enforce with unrecognized contract evaluation semantic. It exists for builds where the reporting machinery itself is unwanted, and it shows: under quick_enforce the demo produces no output at all, not even the lines printed before the violation, because termination is abrupt enough that buffered output never reaches the terminal.

Built with observe, the violated precondition prints a structured report and the program keeps going:

10 / 2 = 5
contract violation in function int divide(int, int) at example.cpp:22: b != 0
  [assertion_kind: pre, semantic: observe, mode: predicate_false, terminating: no]
10 / 0 = 0
still running after the violation

The report names the assertion kind, the semantic in force, why it fired, and whether it is fatal. That structure is deliberate: <contracts> gives you std::contracts::contract_violation, so you can install your own handler and route violations to a logger or a crash reporter instead of the default.

This flag-controlled behaviour is what makes contracts deployable. You can build with observe in staging to collect violations from real traffic without taking the service down, then move to enforce once the reports go quiet. The same source, unmodified.

It cuts the other way too, and the consequence is easy to miss. The semantic is chosen by whoever configures the build, not by whoever wrote the function, so a library author cannot make a precondition check something callers are guaranteed to get. Lucian Radu Teodorescu puts the limitation plainly in the August 2026 Overload: preconditions cannot be fully guaranteed at the code level. A pre states an obligation and offers a way to check it. Whether the check exists in the binary someone ships is a decision made elsewhere.

Where they fit

Contracts overlap with the hardened standard library in spirit and differ in scope: hardening checks the library’s preconditions for you, while contracts let you state your own. Neither replaces tests, because a precondition only fires on inputs you actually execute.

The most useful habit is narrower than “write contracts everywhere”: put a pre on the functions whose misuse is currently documented in a comment. A comment saying “n must be positive” is a contract that nothing checks; pre (n > 0) is the same statement with a compiler and a runtime behind it.


Sources: P2900 “Contracts for C++” · GCC 16 contracts documentation · Lucian Radu Teodorescu, “On Contracts and Safety”, Overload 194, August 2026 · Timur Doumler’s CppCon 2026 session on completing the contract-assertion facility.