P4011R0 - Redefining narrow contract WG21
Posted by u/ewg_paper_feed · 14 hr. ago

Author: Matthias Kretz
Document: P4011R0
Date: 2026-02-12
Target: EWG
Link: wg21.link/p4011r0

Matthias Kretz - yes, the std::simd guy, and current Numerics SG chair - has a short paper arguing that the classic N3248 definitions of "narrow contract" and "wide contract" are broken in a post-P2900 world.

The issue is deceptively simple. Under N3248, a narrow contract is one where calling the function wrong invokes undefined behavior. A wide contract is everything else. But P2900 introduces syntax for preconditions that can be unconditionally enforced. If you enforce a precondition - the function traps or does something defined instead of invoking UB - the function technically has a "wide contract" under N3248, even though it obviously still has a precondition. That's weird.

Kretz's fix: redefine narrow contract as "there exists at least one input for which the call is erroneous." Decouple the definition from undefined behavior entirely. The paper is two pages, one clear problem, one clear fix, and placeholder straw polls. P2900 is the elephant in the room.

▲ 87 points (83% upvoted) · 26 comments
sorted by: best
u/AutoModerator 1 point 14 hr. ago stickied comment

P4011R0 | Redefining narrow contract | Matthias Kretz | EWG | 2026-02-12

I am a bot, and this action was performed automatically. Please contact the moderators if you have questions or concerns.

u/yet_another_cpp_dev 47 points 13 hr. ago

Another day, another contracts paper. At this rate we'll have more papers about defining what a contract is than papers about actual contracts.

u/turbo_llama_9000 89 points 13 hr. ago 🏆

This paper is two pages. I've seen longer commit messages.

u/senior_shitposter_42 51 points 12 hr. ago

Unironically based. Short paper, one clear problem, one clear fix. The committee should try it more often instead of dropping 80-page omnibuses that require a PhD to review.

u/contract_skeptic_2025 34 points 11 hr. ago

The paper identifies a real problem but introduces a new one. The word "erroneous" is doing too much work.

Under C++26, "erroneous behavior" already has formal semantics - it means "well-defined but incorrect, implementations may diagnose." Kretz uses "erroneous" informally. He says explicitly that his definition "does not have to imply erroneous behavior." But then Section 5 connects it to EB anyway:

This is what we expect of EB: it is still detectable as a bug in the program, but the program can continue without invoking UB.

So which is it? Three possible readings:

1. "Erroneous" maps to formal EB. Then narrow = "some inputs trigger erroneous behavior." But EB has specific rules about what implementations can do, and those rules might not apply to all precondition violations.

2. "Erroneous" is a new informal category - "this call is a bug" - with no formal behavioral consequence. Then we've replaced one gap (what happens when you violate a wide-but-preconditioned function?) with another (what does "erroneous" mean if it's not EB and not UB?).

3. "Erroneous" means whatever the contract annotation says, and the evaluation semantics (ignore, observe, enforce) determine the behavioral consequence separately. This is actually the most useful reading, but the paper doesn't commit to it.

The deeper issue: the old definition was operational. You could look at the spec and ask "can this function invoke UB?" and get a yes/no answer. The new definition requires knowing author intent about which inputs are "erroneous." That's a specification question, and the standard doesn't currently have a way to express it without either UB or EB.

R0 problems. But the problem it's trying to solve is real.

Edit: u/lakos_rule_fan raises a good point below about noexcept implications. That might be the bigger omission.

u/lakos_rule_fan 21 points 10 hr. ago

The terminological point is interesting but the practical consequence concerns me more. The entire justification for the Lakos rule - narrow contract functions should not be noexcept - depends on the UB-based definition of narrow. Lakos's argument (N3248, and more recently P2861) is: if calling a function wrong invokes UB, the function must be able to respond in any way, including throwing. Marking it noexcept removes that option.

If we redefine narrow around "erroneous inputs" instead of "UB-producing inputs," do narrow-contract functions still need to avoid noexcept? An unconditionally enforced precondition function can be noexcept just fine - it traps on bad input, no exception needed. But under Kretz's definition it's narrow.

P2861 spent careful effort building noexcept policy on the old definition. This paper needs to at least acknowledge that it's pulling a load-bearing brick from that wall.

u/contract_skeptic_2025 14 points 8 hr. ago

Exactly. And it's not just noexcept. The narrow/wide distinction feeds into [res.on.functions], which tells users "if the standard says UB, it means it." If narrow gets redefined, does that paragraph need updating? Does the library clause change?

This paper opens a bigger box than two pages can close.

u/api_surface_area 18 points 10 hr. ago

Worth zooming out from the definitional debate for a second, because the paper buries its own best insight in a one-sentence consequence:

A human - and potentially also a tool - can tell whether a specific call to a function is a bug in the program or not.

This is actually what we want from a contract definition. Not "what happens to the program" (UB, EB, trap, exception) but "is the caller wrong." Those are different questions. The first is about implementation consequences. The second is about API semantics.

For library documentation, the second question is the one I need answered. When I write docs for sqrt(x), I say "x must be non-negative" - I don't say "passing a negative x invokes undefined behavior." The user needs to know they're wrong. What the implementation does about it is a separate axis.

If you adopt Kretz's definition, static analysis tools get a clearer job description: flag calls where the input can be shown to be erroneous in the precondition sense, regardless of whether the runtime consequence is UB, trap, or a polite error message. That's better for tools like clang-tidy and PVS-Studio than the current definition, where "wide contract" means either "accepts all inputs" or "accepts all inputs because it validates internally" and the tool can't distinguish the two.

Boost.Contract dodged this entire question by having preconditions always throw on violation.

u/lint_my_code_daddy 11 points 9 hr. ago

Static analysis companies rubbing their hands together rn. "Buy our narrow contract detector - now with erroneous-input awareness."

u/old_guard_semantics 16 points 9 hr. ago

This is the reason why some say an unconditionally enforced precondition check is not a contract check anymore.

Those "some" are right, and the paper doesn't engage with their actual argument.

N3248's definition isn't broken - pre!() is the thing that's weird. If you unconditionally enforce a precondition check, you've transformed a specification element into an implementation element. The function now validates its input as part of its observable behavior. That's not a narrow-contract function with a safety net - it's a wide-contract function that rejects bad input. N3248 correctly reflects this transformation.

Three points:

1. A precondition is a promise the caller makes. Whether the callee checks it is an implementation detail. pre!() makes the check part of the function's guaranteed behavior. At that point, the "precondition" is really an input validation step.

2. The paper says we need to "decouple from behavior." But behavior is the discriminant. Narrow means the implementation makes no promises about bad input. Wide means it handles everything. An enforced precondition handles everything - with a specific response. That's wide.

3. The committee spent years getting P2900 through SG21 with layered evaluation semantics (ignore, observe, enforce, quick_enforce). The whole point was that the same annotation can have different behavioral consequences depending on build configuration. That's already the decoupling Kretz wants. The definition doesn't need to change - the understanding of what pre means needs to account for enforcement modes.

u/async_by_default 13 points 8 hr. ago

I'll grant point 3 - P2900's evaluation modes are a form of decoupling. But points 1 and 2 confuse specification with implementation, which is exactly what Kretz is trying to untangle.

A precondition is a specification-level statement: "calling this with x < 0 is a bug in your code." Whether the function detects the bug (enforced) or detonates on it (UB) is an implementation/configuration choice. The N3248 definition conflates these by saying: if the implementation choice is "no UB," then there's no precondition. That's backwards.

Think about it from the caller's perspective. If I call f(-1) and the function traps because pre!(x >= 0) fires, was my call correct? No. Was it a precondition violation? Obviously yes. Does N3248 call this a wide-contract function? Also yes. That's the absurdity the paper names.

Your "input validation" framing in point 1 proves the problem. A function that validates its input and a function with an enforced precondition do different things from the caller's perspective. Validation says "bad inputs are part of my expected domain, I handle them gracefully." A precondition says "bad inputs are YOUR bug, I'm just catching it for you." These have different documentation, different testing strategies, different error handling expectations. The old definition treats them as the same category.

u/old_guard_semantics 9 points 7 hr. ago

Fair point on the caller's perspective distinction - I hadn't considered that angle. Input validation vs. bug-catching enforcement are genuinely different API contracts even if they have the same observable behavior.

But there's a practical consequence the paper ignores. The narrow/wide distinction feeds the Lakos rule: narrow-contract functions shouldn't be noexcept. Under the old definition, this makes sense - if calling wrong invokes UB, the function needs the option to throw in debug mode. Under the new definition, a function with pre!(x >= 0) is narrow but can be noexcept just fine - it traps, no exception needed.

Does Kretz's definition break the Lakos rule? Change it? Make it obsolete? The paper doesn't say, and P2861 built an entire edifice on the UB-based definition.

u/async_by_default 11 points 6 hr. ago

It doesn't break it - it makes it more nuanced. Under the new definition, noexcept guidance becomes two questions instead of one:

1. Is the function narrow-contract? (Does it have erroneous inputs?)
2. What happens on violation? (UB, trap, exception, EB?)

The Lakos rule answered both with one bit: narrow = UB-possible = don't mark noexcept. The new definition splits them. A narrow-contract function with enforced preconditions CAN be noexcept. A narrow-contract function with UB-based preconditions still shouldn't be.

That's strictly more expressive. The paper should say this explicitly, though - you're right that leaving the Lakos rule implications unstated is a real omission for an EWG audience.

u/UB_was_a_mistake 53 points 8 hr. ago

While we're redefining things, can someone redefine "undefined behavior" to mean "the compiler does what you meant, not what you wrote"?

u/definitely_not_a_compiler_dev 22 points 7 hr. ago

erroneous behavior was already supposed to bridge that gap and honestly I'm still not sure what it does

u/constexpr_shitpost 38 points 6 hr. ago

nasal demons but they ask for consent first

u/codegen_matters 12 points 7 hr. ago

From a compiler optimization standpoint, there's a subtlety the encounter thread above is dancing around without naming.

When a function has a narrow contract under the N3248 definition, compilers can assume the precondition holds at every call site. If f(x) requires x >= 0, the compiler can propagate that constraint after the call - dead branch elimination, value range tightening, the works. This is because violating the precondition is UB, and compilers are allowed to assume UB doesn't happen.

If we redefine narrow to include functions where violation is erroneous-but-not-UB (enforced preconditions, EB), compilers lose that assumption for a subset of "narrow" functions. The compiler can't assume x >= 0 after calling a function that traps on x < 0 - because the trap is defined behavior. The program might trap and then continue (depending on the violation handler).

This doesn't mean the new definition is wrong. It means the optimization story bifurcates: "narrow with UB" lets compilers assume, "narrow with enforcement" doesn't. The paper should flag this because it affects how implementers think about the distinction, and EWG includes people who care about codegen.

u/move_semantics_hater 29 points 6 hr. ago

compilers exploiting UB for optimization is how we got into this mess. maybe functions should just trap and we accept the perf hit like adults.

u/daily_template_wizard_2019 8 points 6 hr. ago

Wait, Kretz? Isn't he the simd guy? How did he end up in the contracts tarpit?

u/embedded_for_20_years 14 points 5 hr. ago

He's the Numerics SG chair. Narrow contracts matter for math library interfaces - sqrt of a negative, division by zero, log of zero. Those are precondition violations but the IEEE 754 behavior is perfectly defined. Classic case where N3248 says "wide contract" because no UB, but the caller is still wrong.

[deleted] 5 hr. ago

[deleted]

u/confused_grad_student 3 points 4 hr. ago

what did it say

u/not_a_rust_shill 7 points 4 hr. ago

something about how Rust trait bounds solve this problem and C++ is 40 years behind. the usual.

u/erroneous_by_design 15 points 3 hr. ago

Here "erroneous" does not have to imply erroneous behavior (but see below).

Then, two paragraphs later:

This is what we expect of EB: it is still detectable as a bug in the program, but the program can continue without invoking UB.

Pick one. Either "erroneous" in the definition maps to the standard's EB concept - in which case, say that and accept the implications - or it doesn't, in which case you've introduced an informal category with no formal behavioral semantics.

The g() example in Section 5 is particularly telling. The function has a precondition AND a defined fallback (return 0 for negative input). Under the proposed definition: narrow. Under N3248: depends on whether the fallback means no UB. Under the EB framework: erroneous behavior - well-defined but incorrect. Three frameworks, three potentially different answers for the same function.

R0 is fine for raising the question. But the answer needs to land before EWG can do anything with this.

u/just_ship_it_42 -2 points 2 hr. ago

Placeholder changelog. Placeholder straw polls. Two pages of actual content. Is this a paper or a pull request description?

u/embedded_for_20_years 9 points 1 hr. ago

R0s exist to get early direction from EWG. You don't need full wording to ask "should we redefine this term?" That's literally what R0 is for.