C++ language rules determine whether a program is well-formed and what its execution means. Library named requirements and concepts add contracts for generic code. Similar terminology can hide an important distinction: a compiler can check expressions and types, but it cannot prove every semantic, complexity, validity, or equality promise made by a type.
Definitions and the One-Definition Rule #
Every odr-used non-inline function or variable needs one definition in the program. Declarations can be repeated when compatible, which makes forward declarations possible.
class Node;
struct Node {
Node* next = nullptr;
};Types, templates, inline functions, and inline variables may have definitions in multiple translation units only under their consistency requirements. Header guards prevent repeated inclusion within one translation unit; they do not repair definitions that differ across translation units. A program can compile and link despite an ODR violation, but its behavior is then not reliable.
Named Requirements and Concepts #
Before C++20, standard-library generic contracts were expressed as named requirements such as LegacyInputIterator. C++20 concepts express many of their syntactic portions as constraints.
#include <concepts>
#include <iterator>
template<std::input_iterator Iterator>
void consume(Iterator first, Iterator last);A constrained declaration rejects types that do not satisfy the required expressions and associated type relations. Satisfaction does not establish semantic behavior. A type can make ++i, *i, and i == j well-formed while violating multipass traversal, equality preservation, or a stated complexity bound. The same boundary applies to a uniform random bit generator: type checking cannot demonstrate that its output is uniformly distributed.
Writing Generic Contracts #
Specify a generic API in layers:
- State the required expressions and associated types, preferably with standard concepts or a project concept.
- State semantic invariants: what operations mean, which expressions preserve equality, and whether a value remains valid after a call.
- State complexity, iterator invalidation, ownership, and concurrency guarantees.
- State preconditions and postconditions that runtime callers must uphold or observe.
For example, an equality relation used as a key predicate must be an equivalence relation over the values the algorithm accepts. A hash used with that predicate must produce the same result for equal keys. No requires expression proves either property.
Validity is also a runtime property. An iterator type can meet std::input_iterator syntactically while a particular iterator has been invalidated by a container operation. Document both the type requirement and the operations that invalidate existing iterators, pointers, or references.
Compile-Time Specifiers Have Separate Contracts #
constexpr permits an entity or function to participate in constant evaluation when the invocation and operands satisfy the applicable rules. consteval makes an immediate function call require constant evaluation. constinit applies to a variable with static or thread storage duration and requires constant initialization; it does not make the variable immutable or usable as a constant expression.
constexpr int square(int value) { return value * value; }
consteval int checked_port(int value) { return value > 0 ? value : throw "port"; }
constinit int process_generation = 1;Use these specifiers for their stated guarantees rather than as interchangeable “compile-time” annotations. A const object can be initialized at runtime; a constinit object can later be modified unless it is also const.
Runtime Behavior Remains Part of the Contract #
A well-formed template instantiation does not imply a correct program. Allocation can fail, comparison can throw, a callback can violate reentrancy assumptions, and a range can be empty. State behavior that cannot be represented in the type system, then test it at the boundary where invalid input or external state enters the program.
Complexity is equally contractual. Replacing a constant-time lookup with a linear scan can retain every required expression while breaking an algorithm that relies on its bound. Treat named requirements, concepts, and API documentation as complementary: constraints produce useful diagnostics, while semantic documentation defines the behavior that generic code may depend on.
Comments
Read or comment on GitHub (sign-in required to comment)