Document: P4009R0 · Author: Ville Voutilainen · Date: 2026-02-09 · Audience: EWG
Ville Voutilainen drops a grenade on the C++26 contracts timeline. The proposal rewrites the contracts surface: bare pre(cond) means guaranteed enforce - no configuration can turn it off - while pre(std::pre(cond)) wraps the predicate in a library function that queries a configuration mechanism to pick between enforce, observe, quick_enforce, or ignore. The core language gets only two evaluation semantics: enforce and ignore. All the P2900 complexity moves to library functions.
The motivation is blunt: the Romanian NB comment objects to the lack of guaranteed enforcement, several vendors and prominent members are unhappy, and Voutilainen argues the committee is "at war" with its own constituency by pushing P2900 through as-is. The paper also punts constification to C++29 and admits the implementation-defined configuration mechanism won't work across modules.
Bold move at this stage of C++26. Whether you think this is a reasonable design alternative or DIS-phase arson, it's worth reading.
Paper information for P4009R0:
Title: A proposal for solving all of the contracts concerns
Author: Ville Voutilainen
Document: P4009R0
Date: 2026-02-09
Audience: EWG
I am a bot, and this action was performed automatically. Please contact the moderators if you have any questions or concerns.
we're going to end up with
pre(std::pre(std::pre(pre(x))))and call it progressstd::pre(std::pre(std::pre(...recursive template instantiation depth exceeded. Contracts for C++ has been successfully optimized to C++ for Contracts.compile-time contracts: the assertion is that your build finishes before the heat death of the universe
Did anyone else actually trace through the evaluation model here?
pre(std::pre(x >= 0))When the configuration says "ignore,"
std::pre()returnsstd::ignore. The languageprekeyword sees the return type isstd::ignore_tand uses the "ignore" semantic. Sounds clean.But
std::pre(x >= 0)is a function call.x >= 0is a function argument. It's evaluated beforestd::preruns, regardless of what the configuration says.The entire point of P2900's "ignore" semantic is that the predicate is not evaluated. You have an expensive invariant check like
is_sorted(vec.begin(), vec.end())and in production, ignore means don't pay for it. With this design, you always pay.The paper says "QoI will do what's expected there" for the N-evaluation question, but the evaluation-on-ignore issue isn't QoI - it's a fundamental consequence of the library approach. Unless
std::preis compiler magic masquerading as a library function, which kind of defeats the purpose of making it "library-geared."This is the load-bearing claim of the paper and I don't think it holds weight.
This is the key insight. The paper explicitly says the goal is to "avoid lambdas as a mechanism for deferring evaluation" - but it doesn't actually defer evaluation. It just changes what happens after evaluation. That's a categorically different thing.
P2900's ignore: predicate is not evaluated.
P4009's ignore: predicate is evaluated, result is discarded.
These have very different performance profiles for anything beyond trivial boolean checks.
Right. And it's not just performance. If your predicate has observable side effects - logging, metrics, whatever - P2900-ignore is silent. P4009-ignore still triggers them. That's a semantic difference that library authors care about.
"QoI will do what's expected there" is doing more heavy lifting in this paper than
std::ignoreis.To steelman the approach: the paper's position is that genuinely expensive predicates should use the configurable form, and
std::pre()as a library function could be implemented with compiler support that elides evaluation. The paper says "implementation-defined configuration mechanism" - that gives compilers room to makestd::prespecial.Not saying it's convincing, but "always evaluates" isn't necessarily the design intent - it's the consequence if you read
std::preas a normal function. An implementation could treat it as a builtin.pre(std::pre(x >= 0))is the "simpler" syntax?Am I reading this right?
It's simpler in the same way that C++ is a simple language
To be fair, bare
pre(x >= 0)is still available and means guaranteed enforce. Thestd::pre()wrapping is only for configurable semantics. So the simplest form is simple.The framing as "simpler overall" is a stretch though.
finally, a standard that values job security for those of us who explain contract semantics in code review
So let me get this straight.
We removed contracts from C++20. Spent 5 years redesigning them in SG21. Got P2900 into C++26. And now, in the DIS phase, we're considering tearing it up again?
Is this a standards body or a Sisyphus cosplay?
"extraordinary situation" is doing a lot of heavy lifting in that paper
Dismissing it as process theatrics misses the point. The Romanian NB comment is real and it has formal weight. There are national bodies who have said "we cannot use this feature as designed." That's not a Reddit complaint - it's a ballot-level objection with teeth.
Whether P4009 is the right response is debatable. Whether the concern it addresses is legitimate is not.
Genuine question: has an NB comment ever actually killed a C++ feature at the DIS stage?
From a teaching perspective, I currently explain
pre(cond)as "this function requires cond to be true." One sentence. Students get it.Explaining
pre(std::pre(cond))means I have to cover:1. The language keyword
pre2. A library function
std::pre()that queries a configuration mechanism3. That configuration mechanism picks a semantic (enforce, observe, ignore, quick_enforce)
4. Based on the semantic,
std::pre()returns either a bool orstd::ignore5. The language keyword interprets the return type to decide what to do
That's five abstraction layers for what should be a one-layer concept. Even if students only use the bare form, they'll encounter
std::pre()in every library and every blog post within a month of the feature shipping.the real contract is the friends we confused along the way
Your students should be using bare
pre(cond)then. That form is still there, still simple. Thestd::pre()wrapping exists for organizations that need configurable semantics. Different audiences, different needs.Not everything in the standard needs to be teachable to first-year students.
In theory, sure. In practice, "best practices" guides will standardize on
std::pre()because it's the flexible form. The first Stack Overflow answer will say "always usestd::pre()for production code." And now every student encounters the complex form first.We've seen this with
autoand trailing return types. The simple form exists, but the internet decided the complex form is "correct."every time I think contracts is done, another paper drops that says "actually, have we considered..."
the contracts cinematic universe at this point has more sequels than fast and furious
P2900 is like a TV show that keeps getting renewed when everyone thought the series finale already aired
Speaking as someone who works on a major compiler toolchain: the "implementations are experimental anyway" argument in this paper cuts both ways.
Yes, P2900 implementations aren't production-hardened yet. But we've invested real engineering effort implementing P2900's four evaluation semantics, its interaction with constant evaluation, the violation handler model. "Just change the design" isn't free even for experimental code.
More concerning is this:
That's not a minor caveat. If the feature can't work across the boundaries that C++20 introduced as a headline feature, you're shipping a known incompatibility.
this is why we can't have nice things
The implementation cost argument is overstated. The core language change here is small: two semantics (enforce, ignore) instead of four. That's less to implement, not more. The library part is new work, but library work is inherently lower-risk than core language machinery.
And the module point - yes, the paper acknowledges a limitation. But P2900's interaction with modules isn't exactly solved either. Contract checking across module boundaries has open questions in both designs.
Lower risk? Look at the diagnostic quality story.
The paper proposes
source_locationas a fallback for the expression text that current contract assertions display.source_locationgives you file and line. It does not give youx >= 0 && x < size().The paper's answer is to invent a future "facility that captures code in a nearby context in text form." That's not a solution. That's a TODO comment dressed in committee language.
You're asking implementers to ship something with degraded diagnostics now, on the promise of a fix later. That's the kind of thing that makes compiler teams nervous.
The diagnostic quality concern is legitimate. I'll give you that.
But is your position that we should adopt a more complex language mechanism with four configurable semantics - because the simpler one loses expression text in diagnostics? That's an engineering tradeoff, not a showstopper. Especially since the paper explicitly outlines how implementations can solve it with their own mechanisms today.
I'll concede the core language delta is small. Two semantics is simpler than four, full stop.
But the total system - core language + library functions + implementation-defined configuration + the future diagnostic facility + the future constification keyword + the module interop story - that's not a small delta. That's spreading the complexity across four features and three release cycles, and hoping they all land.
I'd rather have one feature that's self-contained in C++26, even if it's more complex at the language level, than a design that requires C++29 to be complete.
I want to make a distinction that keeps getting lost in these threads.
The Romanian NB comment isn't "contracts bad." It's specifically about the lack of guaranteed enforcement - the ability to say "this contract MUST be checked, always, no build configuration can override this, and the program terminates if it fails."
P2900 has enforce, but any build can configure it away. Bare
pre(cond)in this proposal IS guaranteed enforce - nothing can turn it off. That directly addresses the NB concern.Whether the rest of the P4009 apparatus is worth it is a separate question. But on the specific NB complaint, this paper has an answer that P2900 currently does not.
So the solution to "we want guaranteed enforcement" is an entire design overhaul that also changes configurable enforcement, punts constification, breaks diagnostics, and admits it won't work with modules?
That's using a nuclear submarine to deliver a pizza.
This is exactly right. And it raises the obvious question: could you solve the NB comment with a much smaller change? Add a guaranteed-enforce annotation to P2900 - something like
pre enforce(cond)that can't be configured away - and leave everything else as is.P4009 bundles a targeted fix for the NB concern with a wholesale redesign. The bundling is the problem.
I work at a large financial institution. We have exactly the kind of custom assertion macros the paper describes. Our entire codebase uses
OUR_PRECONDITION(cond)which expands to different checking behavior based on build type, component level, and runtime configuration.pre(our::precondition(cond))is genuinely how we'd want to use a standardized contracts facility. The ability to plug our existing assertion infrastructure into language-level contracts without replacing it is appealing.For shops that already have assertion frameworks like BDE or Abseil CHECK macros, this is a natural fit.
Edit: To be clear, I'm not saying the syntax is pretty. I'm saying the model - language keyword wrapping a library call - maps cleanly to what enterprise codebases actually look like.
But that's the thing - you already have custom macros. This proposal makes the standard syntax longer for everyone else so that shops with existing infra can plug in marginally more cleanly. That's optimizing for the minority case.
Yes Ville, we believe you. Completely fictional. Absolutely no resemblance to any particular financial institution's coding standards whatsoever. None at all.
The constification punt is getting less attention than it deserves.
P2900 makes preconditions const-evaluable - contracts can be checked at compile time. This is the path toward static contract verification, which is arguably the most valuable long-term capability contracts could provide.
This paper says "let's just not do constification" and instead proposes a standalone
constify()keyword in C++29. We're trading a capability we have now for a promise of a different capability three years later.And
constify()as described is a significant language feature on its own. Deferring it to C++29 means it might not land until C++32 if scope creep hits. We've seen that pattern before.Worth noting that constification in P2900 wasn't exactly uncontroversial either. Several committee members felt it was too restrictive. But removing it entirely and promising a replacement is a hard sell when the replacement doesn't exist yet.
The extensibility story is actually interesting, even if the syntax makes me wince.
Right now, if you want custom assertion semantics, you're completely outside the language facility. You use macros. They don't interact with the language's contract checking at all - the compiler can't reason about them, tools can't analyze them, Boost.Contract does its thing entirely at library level.
With
pre(my_custom_function(cond)), the languageprekeyword knows about your custom assertion. It participates in the contract model. That's new. The paper is right that this is "the simplest and most familiar extension mechanism" - it's just a function call.The question is whether the costs (evaluation-on-ignore, diagnostic quality loss, deferred constification, module limitations) are worth that extensibility.
At what cost though?
You lose diagnostic quality (
source_locationis notx >= 0 && x < size()). You lose the ability to skip evaluation. You gain the ability to call a function - which, as you note, we can already do with macros.The paper's answer for diagnostics is to eventually "introduce a new facility that captures code in a nearby context in text form." That's vaporware. It doesn't exist, isn't proposed, and has no timeline.
You're not wrong. The diagnostic quality story is the real achilles heel.
source_locationgives youfoo.cpp:42when what you want isx >= 0 && x < container.size(). The paper openly acknowledges this and punts it."Simply." Right.
I cannot wait for the blog post titled "
pre(std::pre(cond))considered harmful"laughs in undefined behavior
you guys are getting contracts?
we've been negotiating the terms of the contract since 2015
great, another paper that will take 10 years to get through EWG. by the time contracts actually ship, I'll be writing Rust in a retirement home and complaining about borrow checker ergonomics
[removed by moderator]
what did they say?
some crypto link about "decentralized contract verification" or something. you know, the usual
Meanwhile in Rust, every function just has a
requiresattribute and it works. No committees, no five-year redesign cycles, no NB comments. Just ship the feature.But sure, let's spend another three years bikeshedding
pre(std::pre(pre(cond))).Sir, this is a WG21 subreddit
Rust doesn't have a standard contracts feature either. They have third-party crate annotations. Glass houses.