C++ attribute: expects, ensures, assert (C++20)

From cppreference.com
< cpplrm; | languagelrm; | attributes
Attributes
(C++14)
(C++17)
(C++20)(C++20)
expectsensuresassert
(C++20)(C++20)(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

1) Defines a precondition, i.e., the function's expectation of its arguments and/or the state of other objects upon entry into the function. This attribute may be applied to the function type in a function declaration. A precondition is checked by evaluating its predicate immediately before starting evaluation of the function body (including the member initializer list of a constructor). Multiple preconditions of the same function are checked in lexical order.
2) Defines a postcondition, i.e., a condition that a function should ensure for the return value and/or the state of objects upon exit from the function. This attribute may be applied to the function type in a function declaration. The identifier, if present, represents the glvalue result or the prvalue result object of the function, as applicable. A postcondition is checked by evaluating its predicate immediately before returning control to the caller of the function (after the lifetime of local variables and temporaries have ended). Multiple postconditions of the same function are checked in lexical order.
3) Defines an assertion, i.e., a condition that should be satisfied where it appears in a function body. This attribute may be applied to a null statement. An assertion is checked by evaluating its predicate as part of the evaluation of the null statement it applies to.

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. However, friendship is not considered for names appearing in a contract condition. Additionally:

  • A contract condition on a public member function can only access public members of the class and members of a base class that is accessible as a public member of the class;
  • A contract condition on a protected member function can only access public or protected members of the class and members of a base class that is accessible as a public or protected member of the class.
class X {
public:
    int v() const { return x; }
    int f() [[expects: x > 0]]; // error: x is private
    int g() [[expects: v() > 0]]; // OK
    friend void r(X& o, int z) [[expects: o.x > z]]; // error: x is private
private:
    int k() [[expects: x > 0]]; // OK
    int x;
};

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
}

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 or audit.

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.