Historical Context & Motivation
The idea that software components should carry explicit promises about their behavior did not originate with any single programming language — it grew from decades of formal methods research. In the 1960s and 1970s, as programs became too complex for ad hoc testing alone, computer scientists began searching for mathematical tools that could guarantee correctness before a single test was run. The central insight was deceptively simple: if every function declares exactly what it needs (preconditions), what it promises to deliver (postconditions), and what must remain true throughout (invariants), then reasoning about large systems reduces to reasoning about small, composable units.
Expects() and Ensures() macros from the Guidelines Support Library (GSL) as idiomatic ways to express contracts.pre, post, and contract_assert attributes is on track for the C++26 standard, giving the language first-class contract support.Despite this long history, most C++ code in production today relies on comments, naming conventions, or nothing at all to communicate contracts. This gap between what a function assumes and what a caller provides is the single largest source of undefined behavior in real-world C++ applications. The question this lesson addresses is straightforward: how do we state, check, and enforce the contract of every function we write?
Core Principles & Definitions
A function contract is the complete specification of the agreement between a function and its callers. It consists of three parts: what must be true when the function is called (preconditions), what must be true when it returns (postconditions), and what must remain true across the function's execution without being violated at any observable point (invariants). Understanding these three pillars is essential for writing robust, self-documenting C++ code that composes safely in large systems.
Preconditions
sqrt(x) function requires x >= 0.Postconditions
sort(v) guarantees the output is a permutation of the input in non-decreasing order.Class Invariants
BankAccount always has balance_ >= 0.Loop Invariants
Wide vs. Narrow Contracts
Visual Explanation — Anatomy of a Function Contract
Notice that the diagram draws a sharp boundary between the caller's responsibilities and the function's responsibilities. This boundary is the contract itself. In C++, unlike in Eiffel, the contract is traditionally implicit — communicated through documentation comments, naming conventions (e.g., noexcept, [[nodiscard]]), and defensive programming with assert(). Modern C++ is moving toward making these contracts explicit through language features and library tools, but the conceptual model remains the same: every function has a contract whether you state it or not, and the only question is whether you make it legible to humans and machines.
How It Works — Formal Notation and C++ Expression
The formal foundation for function contracts comes from Hoare logic, which gives us a notation for reasoning about program correctness. The Hoare triple relates a precondition P, a command (or function body) C, and a postcondition Q. If P holds before C executes, then Q is guaranteed to hold after C completes. This framework maps directly to how we write contracts in C++.
Expressing Contracts in C++
C++ provides several mechanisms for expressing contracts, ranging from comments to compile-time checks. The most common approach today uses assert() from <cassert> for runtime checks, static_assert() for compile-time checks, and the GSL macros Expects() and Ensures() for semantically clearer precondition and postcondition assertions. The upcoming C++26 contracts facility introduces pre and post attributes directly in function declarations, bringing contracts into the type system.
| Mechanism | Syntax Example | When Checked |
|---|---|---|
assert() | assert(n > 0); | Runtime (debug builds only; removed by NDEBUG) |
static_assert() | static_assert(sizeof(int) >= 4); | Compile time |
GSL Expects() | Expects(ptr != nullptr); | Runtime (always, unless configured otherwise) |
GSL Ensures() | Ensures(result >= 0); | Runtime (always, unless configured otherwise) |
C++26 pre/post | int f(int x) pre(x > 0) post(r: r >= 0); | Configurable: ignore, observe, or enforce |
Contract Types and Invariant Classification
Contracts and invariants come in several flavors, each serving a different purpose in the design of correct software. Understanding the taxonomy helps you decide which tools to reach for in different scenarios. A precondition on a free function is fundamentally different from a class invariant on a container, even though both fall under the umbrella of "contracts." Similarly, a representation invariant (constraining private data) differs from an abstraction invariant (constraining the logical model visible to clients).
The distinction between wide contracts and narrow contracts has practical implications for API design. A function like std::vector::at() has a wide contract: it accepts any index and throws std::out_of_range for invalid ones. In contrast, std::vector::operator[]() has a narrow contract: calling it with an out-of-bounds index is undefined behavior. The C++ Standard Library uses narrow contracts pervasively for performance-critical paths, relying on the caller to satisfy preconditions. This is a deliberate design choice: checking preconditions costs time, and in performance-sensitive code, the contract shifts that responsibility to the caller in exchange for zero overhead.
Worked Example — Designing a Contract-Rich Class
Let us design a simple BoundedStack<T, N> class — a stack with a compile-time maximum capacity — and systematically identify its preconditions, postconditions, and invariants. This example demonstrates how to translate informal requirements into explicit, checkable contracts.
BoundedStack stores up to N elements. The internal state is an array data_[N] and a count size_. The class invariant is: 0 <= size_ && size_ <= N and elements data_[0..size_-1] are valid, constructed objects of type T.0 <= size_ <= N ∧ all data_[0..size_−1] are valid Tpush(const T& value) method requires that the stack is not full. Its postcondition guarantees that the new top element equals the pushed value and the size has increased by one. We also assert the invariant holds on exit.size_ < N | POST: top() == value && size() == old_size + 1 | INV preservedpop() method requires that the stack is non-empty. Its postcondition guarantees that the size has decreased by one. Note that in C++ we typically separate top() (query) from pop() (command) for exception safety — this is itself a contract-driven design decision.size_ > 0 | POST: size() == old_size − 1 | INV preservedtemplate <typename T, std::size_t N>
class BoundedStack {
std::array<T, N> data_{};
std::size_t size_ = 0;
bool check_invariant() const {
return size_ <= N;
}
public:
void push(const T& value) {
Expects(size_ < N); // precondition
data_[size_] = value;
++size_;
Ensures(data_[size_ - 1] == value); // postcondition
Ensures(check_invariant()); // invariant
}
void pop() {
Expects(size_ > 0); // precondition
--size_;
Ensures(check_invariant()); // invariant
}
const T& top() const {
Expects(size_ > 0); // precondition
return data_[size_ - 1];
}
bool empty() const noexcept { return size_ == 0; } // wide contract
bool full() const noexcept { return size_ == N; } // wide contract
std::size_t size() const noexcept { return size_; } // wide contract
};empty(), full(), size()) have wide contracts — they accept any call and return a defined result.// Loop invariant: all elements above size() have been processed.
while (!stack.empty()) {
process(stack.top());
stack.pop();
}
// Post: stack.size() == 0
The loop invariant here is that at each iteration start, all elements from the original top down to the current size() have been processed. The loop condition !empty() satisfies pop()'s precondition, and termination is guaranteed because size() strictly decreases each iteration.Strengths, Limitations, and Tradeoffs
Like any engineering discipline, enforcing function contracts involves tradeoffs. Runtime assertion checks impose a performance cost; overly restrictive preconditions can reduce API flexibility; and stating invariants formally requires effort that teams may resist. Understanding these tradeoffs helps you calibrate your approach based on the project's risk profile, performance requirements, and team culture.
| Aspect | Strengths | Limitations |
|---|---|---|
| Bug Detection | Precondition and invariant checks catch contract violations early, often at the exact call site, rather than propagating to distant failures. | Runtime checks only catch violations that are actually executed. They do not prove absence of bugs across all inputs. |
| Documentation | Contracts serve as machine-readable documentation that stays synchronized with the code — unlike comments, they fail when wrong. | Complex contracts (e.g., "the input graph is a DAG") may be expensive or impossible to check at runtime. |
| Performance | Narrow contracts allow zero-overhead implementations: if the caller guarantees preconditions, the function skips validation. | Removing assertions in release builds (NDEBUG) means production code runs without safety nets; contract violations become UB. |
| Composability | When every function states its contract, reasoning about chains of calls becomes modular — each link's postcondition feeds the next link's precondition. | Inheritance can weaken contracts: a derived class can strengthen preconditions (violating LSP) if the developer is not careful. |
| Tooling | Static analyzers, sanitizers, and formal verification tools can leverage declared contracts to prune search spaces and generate targeted tests. | Until C++26 contracts ship widely, the ecosystem is fragmented: GSL, custom macros, and assert() each have different semantics and limitations. |
Connection to Advanced Theory — Formal Verification and Beyond
The contracts we have discussed so far are lightweight: they are runtime checks that sample correctness at specific program points. Advanced formal methods extend this idea to prove that contracts hold for all possible inputs, not just the ones encountered during testing. Tools like Frama-C (for C), SPARK/Ada, and theorem provers like Coq and Lean take Hoare-style reasoning to its logical conclusion: mathematical proof of total correctness. The Liskov Substitution Principle (LSP) connects contracts to object-oriented design by requiring that subtypes preserve the contracts of their supertypes — specifically, preconditions may be weakened but never strengthened, and postconditions may be strengthened but never weakened.
| Feature | Runtime Contracts (this lesson) | Formal Verification (advanced) |
|---|---|---|
| Guarantee | Checks hold for executed paths only | Proves contracts hold for all reachable states |
| Effort | Low — a few lines of assertions per function | High — formal specifications and proof obligations |
| Expressiveness | Limited to efficiently checkable boolean expressions | Can express quantified properties (∀, ∃), temporal logic, etc. |
| C++ Support | assert(), GSL, C++26 pre/post | Limited — research tools like SAW, CBMC, or external verification |
| Best For | Application and library code where testing suffices | Safety-critical systems (avionics, medical devices, cryptography) |
As you advance in software engineering, you will encounter systems where runtime contracts are necessary but not sufficient. The skills you are building now — precisely stating what a function requires and guarantees — are the same skills needed to write formal specifications. In that sense, learning to state contracts is the first step on the path from testing-based confidence to proof-based certainty.
Practice Problems
int binary_search(const std::vector<int>& v, int target), state the preconditions and postconditions. Write them using Expects() and Ensures() or C++26 pre/post syntax.CircularBuffer<T, N> class with methods enqueue(const T&) and dequeue(). State the class invariant, and for each method, state preconditions and postconditions. Identify whether each method has a wide or narrow contract.std::string format_currency(double amount, int decimal_places) for a financial application. State the complete contract. Then explain why you might prefer a wide contract (returning an error string or throwing) over a narrow contract (UB on invalid inputs) in this domain, referencing the risk profile of financial software.Shape with a virtual method double area() const whose postcondition is result >= 0. A derived class Rectangle adds the postcondition result == width_ * height_. Does Rectangle satisfy LSP? Now suppose a class WeirdShape overrides area() to return negative values for certain inputs. Analyze the LSP violation and explain its consequences for code that depends on the Shape contract.Summary — Function Contracts & Invariants
A function contract consists of three components: preconditions that the caller must satisfy before invocation, postconditions that the function guarantees on return, and invariants that must hold at designated program points (after construction, between method calls, at each loop iteration). These concepts trace back to Hoare logic (1969) and were refined by Bertrand Meyer's Design by Contract (1986). In C++, contracts can be expressed using assert(), the GSL's Expects()/Ensures(), or the upcoming C++26 pre/post attributes.
The distinction between wide contracts (which define behavior for all inputs) and narrow contracts (which have preconditions and invoke UB when violated) is central to C++ API design and performance engineering. Class invariants ensure object consistency across their lifetime, while loop invariants bridge preconditions to postconditions through iterative computation. The Liskov Substitution Principle extends contract reasoning to inheritance hierarchies: subtypes may weaken preconditions and strengthen postconditions, but never the reverse. Mastering contracts is the foundation for writing correct, composable, and self-documenting C++ code.