C++ attribute: expects, ensures, assert (C++20)
Specifies preconditions, postconditions, and assertions for functions.
Syntax
[[ expects contract-level(optional) : expression ]]
|
(1) | (since C++20) | |||||||
[[ ensures contract-level(optional) identifier(optional) : expression ]]
|
(2) | (since C++20) | |||||||
[[ assert contract-level(optional) : expression ]]
|
(3) | (since C++20) | |||||||
contract-level | - | one of default , audit , or axiom ; the default is default
|
identifier | - | an identifier that is taken to denote the return value of the function; any ambiguity on whether something is a contract-level or an identifier is resolved in favor of it being a contract-level |
expression | - | an expression, contextually converted to bool, that specifies the predicate of the contract; its top-level operator may not be an assignment or comma operator |
Explanation
The expression in a contract attribute, contextually converted to bool, is called its predicate. Evaluation of the predicate must not have any side effects other than modification of non-volatile objects whose lifetimes begin and end within that evaluation; otherwise the behavior is undefined. If the evaluation of a predicate exits via an exception, std::terminate is called.
During constant expression evaluation, only predicates of checked contracts are evaluated. In all other contexts, it is unspecified whether the predicate of a contract that is not checked is evaluated; the behavior is undefined if it would evaluate to false.
Contract conditions
Preconditions and postconditions are collectively called contract conditions. These attributes may be applied to the function type in a function declaration:
int f(int i) [[expects: i > 0]] [[ensures audit x: x < 1]]; int (*fp)(int i) [[expects: i > 0]]; // error: not a function declaration
The first declaration of a function must specify all contract conditions (if any) of the function. Subsequent redeclarations must either specify no contract conditions or the same list of contract conditions; no diagnostic is required if corresponding conditions will always evaluate to the same value. If the same function is declared in two different translation units, the list of contract conditions shall be the same; no diagnostic is required.
Two lists of contract conditions are the same if they contain the same contract conditions in the same order. Two contract conditions are the same if they are the same kind of contract condition and have the same contract-level and the same predicate. Two predicates are the same if they would satisfy the one-definition rule were they to appear in function definitions, except for the renaming of function and template parameters and return value identifiers (if any).
int f(int i) [[expects: i > 0]]; int f(int); // OK, redeclaration int f(int j) [[expects: j > 0]]; // OK, redeclaration int f(int k) [[expects: k > 1]]; // ill-formed int f(int l) [[expects: 0 < l]]; // ill-formed, no diagnostic required
If a friend declaration is the first declaration of the function in a translation unit and has a contract condition, that declaration must be a definition and must be the only declaration of the function in the translation unit:
struct C { bool ok() const; friend void f(const C& c) [[ensures: c.ok()]]; // error, not a definition friend void g(C c) [[expects: c.ok()]] { } // OK }; void g(C c); // error
The predicate of a contract condition has the same semantic restrictions as if it appeared as the first expression statement in the body of the function it applies to.
If a postcondition odr-uses a parameter in its predicate and the function body modifies the value of that parameter directly or indirectly, the behavior is undefined.
int f(int x) [[ensures r: r == x]] { return ++x; // undefined behavior } int g(int* p) [[ensures: p != nullptr]] { *p = 42; // OK, p is not modified } bool meow(const int&) { return true; } void h(int x) [[ensures: meow(x)]] { ++x; // undefined behavior } void i(int& x) [[ensures: meow(x)]] { ++x; // OK; the "value" of a reference is its referent and cannot be modified }
For templated functions with deduced return types, the return value may be named in a postcondition without additional restrictions (except that the name of the return value is treated as having a dependent type). For the non-templated functions with deduced return types, naming the return value is prohibited in declarations (but allowed in the definitions):
auto h(int x) [[ensures res: true]]; // error: return value with deduced type // on a non-template function declaration
Build level and violation handling
A program may be translated with one of three build levels:
- off: no contract checking is performed.
- default (default if no build level is selected): checking is performed for contracts whose contract-level is
default
. - audit: checking is performed for contracts whose contract-level is
default
oraudit
.
The mechanism for selecting the build level is implementation-defined. Combining translation units that were translated at different build levels is conditionally-supported.
The violation handler for a program is a function of type void (const std::contract_violation &) (optionally noexcept), specified in an implementation-defined manner. It is invoked when the predicate of a checked contract evaluates to false.
- If a precondition is violated, the source location reflected in the std::contract_violation argument is implementation-defined.
- If a postcondition is violated, the source location reflected in the std::contract_violation argument is the source location of the function definition.
- If an assertion is violated, the source location reflected in the std::contract_violation argument is the source location of the statement to which the assertion is applied.
The value of the std::contract_violation argument passed to the violation handler is otherwise implementation-defined.
If a violation handler exits by throwing an exception and a contract is violated on a call to a function with a non-throwing exception specification, std::terminate is called:
void f(int x) noexcept [[expects: x > 0]]; void g() { f(0); // terminate if the violation handler throws }
A program may be translated with one of two violation continuation modes:
- off (default if no continuation mode is selected): after the execution of the violation handler completes, std::terminate is called;
- on: after the execution of the violation handler completes, execution continues normally.
Implementations are encouraged to not provide any programmatic way to query, set, or modify the build level or to set or modify the violation handler.