Skip to content
HN On Hacker News ↗

C++26 Contracts: Introduction to Preconditions and Postconditions

▲ 151 points • 188 comments • by ibobev • 3w ago • HN discussion ↗

Pangram verdict · v3.3

We believe that this text is a mix of AI and human-written content.

39 %

AI likelihood · overall

Mixed
71% human-written 29% AI-generated
SEGMENTS · HUMAN 5 of 10
SEGMENTS · AI 4 of 10
WORD COUNT 1,224
PEAK AI % 94% · §4
Analyzed
Oct 15
backend: pangram/v3.3
Segments scanned
10 windows
avg 122 words each
Distribution
71 / 29%
human / AI fraction
Verdict
Mixed
Pangram v3.3

Article text · 1,224 words · 10 segments analyzed

Human AI-generated
§1 Human · 20%

Table of Contents Starting with an Assertion Adding a Precondition Why Use Contracts Instead of assert? A Complete Precondition Example Contracts on Declarations and Definitions Contract Evaluation Semantics Compiling with GCC Adding a Postcondition Const Parameters in Postconditions Assertions Inside a Function Avoiding Mutation in Predicates Contracts Are Not Input Validation Considering Predicate Costs Summary In the article about Standard Library hardening, we looked at checks for invalid Standard Library container access and other broken preconditions. I mentioned that those requirements are expressed in terms of “contracts”. But what about our code? In this post, we’ll explore contracts from C++26. We’ll start with a simple cassert, move the requirement into the function itself, and see how to check results. We’ll also look at declarations and definitions, compiler options, and a few details that can surprise you. Starting with an Assertion Let’s start with a small example: A header file first: // foo.h #include <vector> int get_value(const std::vector<int>& values, std::size_t index); And the implementation: // foo.cpp #include "foo.h" // our function declaration... #include <cassert> int get_value(const std::vector<int>& values, std::size_t index) { assert(index < values.size()); return values[index]; } The function expects a valid index.

§2 AI · 72%

If the condition is false, assert prints a diagnostic message and terminates the program. However, this only happens when assertions are enabled. Defining NDEBUG disables the check and basically removes the check from the code. What are the issues with this simple approach? The requirement lives inside the implementation. Somebody reading only the declaration sees a vector and an index, without the relationship between them. Adding a Precondition With C++26 contracts, we can move that requirement from an assertion inside the function body to the function itself: int get_value(const std::vector<int>& values, std::size_t index) pre(index < values.size()) { return values[index]; } Notice the new pre(...) syntax after the parameter list. It introduces a precondition assertion: a condition expected to hold when entering the function.

§3 Human · 28%

What’s more, we can add this precondition to the declaration: int get_value(const std::vector<int>& values, std::size_t index) pre(index < values.size()); The feature was introduced through P2900, Contracts for C++. Let’s understand how it works and how it differs from assert.

§4 AI · 94%

Why Use Contracts Instead of assert? At first, pre(index < values.size()) might look like another way to write assert(index < values.size()). Both express a condition that should be true. So what’s the benefit of using contracts? There are a few important differences: Requirements in declarations: We can put pre and post directly in the function declaration. This makes the requirements visible to anyone reading the API, without looking at the implementation. More flexible checking: Traditional assert is enabled or disabled through NDEBUG. Contracts define several evaluation semantics, including ignoring a check, reporting a violation and continuing, or terminating the program. The compiler implementation determines which semantics are available and how they can be selected. Preconditions and postconditions: Contracts distinguish between what the caller must provide and what the function promises to return. With assert, we would have to place checks manually inside the function body, including checks before different return statements. This also makes contracts useful for documenting APIs, even when runtime checking is disabled.

§5 Human · 21%

A Complete Precondition Example Here’s a complete example (no header file for simplicity for now): #include <print> #include <vector> int get_value(const std::vector<int>& values, std::size_t index) pre(index < values.size()) { return values[index]; } int main() { std::vector<int> values { 10, 20, 30 }; std::println("{}", get_value(values, 1)); // get_value(values, 10); // violates the precondition } The valid call prints: 20 And here’s the version for you to play with the problematic call uncommented: See at Compiler Explorer On GCC, I’m getting: Program returned: 134 Program stdout 20 Program stderr contract violation in function int get_value(const std::vector<int>&, std::size_t) at /app/example.cpp:6: index < values.size() [assertion_kind: pre, semantic: enforce, mode: predicate_false, terminating: yes] terminate called without an active exception Program terminated with signal SIGABRT (6) Contracts on Declarations and Definitions For clarity, contracts can appear on a declaration, in a definition, or both: int get_value(const std::vector<int>& values, std::size_t index) pre(index < values.size()); The definition can then omit the contract specifier: int get_value(const std::vector<int>& values, std::size_t index) { return values[index]; } The contract assertions are established by the function’s first declaration. A later declaration or definition may omit the contract specifiers, as above, or repeat the same sequence. A later declaration cannot introduce a different set of preconditions or postconditions. In practice, if a function is declared in a header, that first declaration is the natural place for its preconditions and postconditions. If you’re reading the API, it’s probably best to see all preconditions without looking inside the implementation. What’s more, contracts are associated with the function, but they are not part of the function type. Overload resolution, for example, does not distinguish functions based on their contracts. Contract Evaluation Semantics C++26 defines four evaluation semantics: Semantic What happens at runtime? Ignore The predicate is not evaluated.

§6 Mixed · 67%

Observe A violation invokes the handler; execution continues if it returns normally. Enforce A violation invokes the handler; execution terminates if it returns normally. Quick-enforce A violation terminates execution without invoking the handler. The implementation determines which semantic applies. C++26 has no portable syntax for forcing an individual ordinary contract assertion to use one specific policy. These rules appear in the contract evaluation specification. With the ignore semantic, no runtime check is performed. With observe, a violation is reported, but execution continues if the handler returns normally. In our example, both policies can allow the program to reach values[index] with an invalid index, resulting in undefined behavior. Thus, adding pre(...) expresses the requirement, while the selected policy determines how checking responds.

§7 Human · 28%

Compiling with GCC As of October 2026, only GCC 16 supports Contracts. We can use it with special compile flags: g++ -std=c++26 -fcontracts \ -fcontract-evaluation-semantic=enforce example.cpp With that policy, the invalid call invokes the violation handler. If the handler returns normally, execution terminates before the function body performs the invalid access. GCC’s default handler emits diagnostic information. See the GCC options documentation - contracts. GCC also allows us to replace the default contract-violation handler. For example, we can log a custom message when a violation is detected. The handler receives a std::contracts::contract_violation object, which provides information about the failed contract, including the predicate, location, and evaluation semantic. #include <contracts> #include <print> void handle_contract_violation( const std::contracts::contract_violation& violation) { std::println(stderr, "Contract violated: {}", violation.comment()); } See an example @Compiler Explorer Notice that replacing the handler doesn’t change what happens after it returns.

§8 AI · 75%

With observe, execution continues. With enforce, the program terminates. Not every implementation is required to support replacing the handler, so this feature is implementation-defined. Adding a Postcondition So far, we’ve described what the caller must provide. We can also describe what a function promises when it returns normally.

§9 Human · 21%

See this example: int clamp_value(int value, const int low, const int high) pre(low <= high) post(result: result >= low && result <= high) { if (value < low) return low; if (value > high) return high; return value; } There are two useful pieces here: pre(low <= high) checks that the bounds form a valid interval.

§10 AI · 83%

post(result: ...) checks that the returned value lies inside that interval. The name result is ours to choose. It refers to the function’s result within that postcondition. What’s convenient is that the same postcondition covers every normal return path. We don’t have to repeat an assertion before each return.