C++26 Contracts: Introduction to Preconditions and Postconditions
C++26 Contracts: A Guide to Preconditions and Postconditions
In previous discussions regarding Standard Library hardening, we explored how the language handles broken preconditions and invalid container access. Those requirements are fundamentally based on the concept of contracts.
In this guide, we will transition from using basic cassert calls to integrating requirements directly into function signatures, while exploring the nuances of declarations, definitions, and compiler-driven evaluation.
🛠️ Starting with Traditional Assertions
Consider a scenario where we need to retrieve a value from a std::vector.
The Header (foo.h):
#include <vector>
int get_value(const std::vector<int> values, std::size_t index);
The Implementation (foo.cpp):
#include "foo.h"
#include <cassert>
int get_value(const std::vector<int> values, std::size_t index) {
// Ensure the index is within bounds
assert(index < values.size());
return values[index];
}
In this traditional approach, the function assumes the index is valid. If the condition is false, assert triggers a diagnostic and kills the process. However, this has drawbacks:
, leaving the code vulnerable in release builds.NDEBUGcompletely erases the check- The API is opaque; a developer looking only at the header has no idea that
indexmust be within the vector's range.
🚀 Introducing C++26 Contracts
C++26 (via proposal P2900) allows us to move these requirements from the function body into the function's signature using the pre(...) syntax.
int get_value(const std::vector<int> values, std::size_t index)
pre(index < values.size())
{
return values[index];
}
This introduces a precondition assertion: a formal requirement that must be true before the function executes. Crucially, this can be placed directly in the declaration:
int get_value(const std::vector<int> values, std::size_t index) pre(index < values.size());
Why is this better than assert?
While pre() looks like a fancy assert(), it offers several distinct advantages:
- API Visibility: Requirements are visible in the declaration. You don't need to dig into the
.cppfile to understand the constraints. - Granular Control: Unlike the binary nature of
NDEBUG, contracts support various evaluation semantics (detailed below). - Semantic Clarity: Contracts explicitly distinguish between what the caller must guarantee (preconditions) and what the function guarantees to provide (postconditions).
💻 Full Implementation Example
Here is a complete, runnable example demonstrating a contract violation.
#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};
// This call is valid
std::println("{}", get_value(values, 1));
// This call violates the precondition:
// get_value(values, 10);
}
Execution Flow
The logic for a contract check can be visualized as follows:
When running a violation on GCC, the output typically looks like this:
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 exceptionProgram terminated with signal SIGABRT (6)
📋 Declarations vs. Definitions
Contracts follow specific rules regarding where they are placed:
- The First Declaration Wins: The first time a function is declared, the contracts are established.
- Optional Repetition: Subsequent declarations or the final definition can either repeat the contracts or omit them entirely.
- No Modifications: You cannot change or add new preconditions in a later declaration.
Example:
// Header: The "Source of Truth"
int get_value(const std::vector<int> values, std::size_t index) pre(index < values.size());
// Implementation: Contract is omitted but still active
int get_value(const std::vector<int> values, std::size_t index) {
return values[index];
}
[!IMPORTANT] Contracts are associated with the function but are not part of the function's type. This means they do not influence overload resolution.
⚙️ Contract Evaluation Semantics
C++26 provides different ways to handle violations. While there is no portable syntax to set these per-assertion, the compiler implementation determines the available policies.
| Semantic | Runtime Behavior |
|---|---|
Observe | Invokes the handler; continues execution if the handler returns normally. |
Enforce | Invokes the handler; terminates the program if the handler returns normally. |
Quick-enforce | Terminates the program immediately without calling the handler. |
Warning: Using the Observe policy can be dangerous. If a violation is reported but execution continues, the program may proceed to execute values[index] with an invalid index, leading to Undefined Behavior (UB).
Summary Checklist
- Move assertions to
pre()for better API documentation. - Define contracts in the first declaration (usually the header).
- Understand that contracts function types.
- Be cautious of the
Observesemantic to avoid UB.
![]()