Skip to content

Contracts

Overview

Cpp2 currently supports three kinds of contracts:

  • Preconditions and postconditions. A function declaration can include pre(condition) and post(condition) before the = /* function body */. Before entering the function body, preconditions are fully evaluated and postconditions are captured as function expressions to be evaluated later (and perform their captures of values on entry, if any). Immediately before exiting the function body via a normal return, postconditions are evaluated. If the function exits via an exception, postconditions are not evaluated.

  • Assertions. A function body can write assert(condition) assertion statements. Assertions are evaluated when control flow passes through them.

Notes:

  • condition is an expression that evaluates to true or false. It will not be evaluated unless checking for this contract group is enabled (group.is_active() is true).

  • Optionally, condition may be followed by , "message", a message to include if a violation occurs. For example, pre(condition, "message").

  • Optionally, a <group, pred1, pred2> can be written inside < > angle brackets immediately before the (, to designate that this test is part of the contract group named group and (also optionally) contract predicates pred1 and pred2. If a violation occurs, Group.report_violation() will be called. For example, pre<group>(condition). If no contract group is specified, the contract defaults to being part of the default group (spelled cpp2_default when used from Cpp1 code).

The order of evaluation is:

  • First, if the contract group is unevaluated then the contract is ignored; condition is never evaluated. This special group designates conditions intended for use by static analyzers only, and the only requirement is that the condition be grammatically valid.

  • Next, predicates are evaluated in order. If any predicate evaluates to false, stop.

  • Next, group.is_active() is evaluated. If that evaluates to false, stop.

  • Next, condition is evaluated. If that evaluates to true, stop.

  • Finally, if all the predicates were true and the group is active and the condition was false, group.report_violation() is called.

For example:

Precondition and postcondition examples
insert_at: (container, where: int, val: int)
    pre<bounds_safety>( 0 <= where <= container.ssize(), "position (where)$ is outside 'container'" )
    post              ( container.ssize() == container.ssize()$ + 1 )
= {
    _ = container.insert( container.begin()+where, val );
}

In this example:

  • The $ captures are performed before entering the function.

  • The precondition is part of the bounds_safety contract group and is checked before entering the function. If the check fails, say because where is -1, then cpp2::bounds_safety.report_violation("position -1 is outside 'container'") is called.

  • The postcondition is part of the default safety contract group. If the check fails, then cpp2::default.report_violation() is called.

Contract groups

Contract groups are useful to enable or disable or set custom handlers independently for different groups of contracts. A contract group grp is just the name of an object that can be called with:

  • grp.report_violation() and grp.report_violation(message), where message is a * const char C-style text string

  • grp.is_active(), which returns true if and only if the group is enabled

You can create new contract groups just by creating new objects that have a .report_violation function. The object's name is the contract group's name. The object can be at any scope: local, global, or heap.

For example, here are some ways to use contract groups of type cpp2::contract_group, which is a convenient group type:

Using contract groups
group_a: cpp2::contract_group = ();          // a global group

func: () = {
    group_b: cpp2::contract_group = ();      // a local group

    group_c := new<cpp2::contract_group>();  // a dynamically allocated group

    // ...

    assert<group_a >( some    && condition );
    assert<group_g >( another || condition );
    assert<group_c*>( another && condition );
}

You can make all the objects in a class hierarchy into a contract group by having a .report_violation function in a base class, and then writing contracts in that hierarchy using <this> as desired. This technique is used in cppfront's own reflection API:

Example of using 'this' as a contract group, from cppfront 'reflect.h2'
function_declaration: @copyable type =
{
    // inherits from a base class that provides '.report_violation'

    // ...

    add_initializer: (inout this, source: std::string_view)
        pre<this> (!has_initializer(), "cannot add an initializer to a function that already has one")
        pre<this> (parent_is_type(),   "cannot add an initializer to a function that isn't in a type scope")
    = { /*...*/ }

    // ...

}

Contract predicates

Contract predicates are useful to conditionally check specific contracts as a static or dynamic property. Importantly, if any predicate is false, the check's conditional expression will not be evaluated.

For example:

Using contract predicates
is_checked_build: bool == SEE_BUILD_FLAG;   // a static (compile-time) predicate

checking_enabled: bool =  /*...*/ ;         // a dynamic (run-time) predicate,
                                            // could change as the program runs

func: () = {
    assert<audit, is_checked_build, checking_enabled>( condition );
}

In this example, the order of evaluation is:

  • is_checked_build is evaluated. Since it is a compile-time value, the evaluation can happen at compile time. If it evaluates to false, then stop; the entire contract could be optimized away by the compiler.

  • Otherwise, next checking_enabled is evaluated at run time. If it evaluates to false, then stop.

  • Otherwise, next audit.is_active() is evaluated. If it evaluates to false, then stop.

  • Otherwise, next condition is evaluated. If it evaluates to true, then stop.

  • Otherwise, audit.report_violation() is called.

cpp2::contract_group, and customizable violation handling

The contract group object could also provide additional functionality. For example, Cpp2 comes with the cpp2::contract_group type which allows installing a customizable handler for each object. Each object can only have one handler at a time, but the handler can change during the course of the program. contract_group supports:

  • .set_handler(pfunc) accepts a pointer to a handler function with signature * (* const char).

  • .get_handler() returns the current handler function pointer, or null if none is installed.

  • .is_active() returns whether there is a current handler installed.

  • .enforce(condition, message) evaluates condition, and if it is false then calls .report_violation(message).

Cpp2 comes with five predefined contract group global objects in namespace cpp2:

  • default, which is used as the default contract group for contracts that don't specify a group.

  • type_safety for type safety checks.

  • bounds_safety for bounds safety checks.

  • null_safety for null safety checks.

  • testing for general test checks.

For these groups, the default handler is cpp2::report_and_terminate, which prints information about the violation to std::cerr and then calls std::terminate(). But you can customize it to do anything you want, including to integrate with any third-party or in-house error reporting system your project is already using. For example:

Example of customized contract violation handler
main: () -> int = {
    cpp2::default.set_handler(call_my_framework&);
    assert<default>(false, "this is a test, this is only a test");
    std::cout << "done\n";
}

call_my_framework: (msg: * const char) = {
    //  You can do anything you like here, including arbitrary work
    //  and integration with your current error reporting libraries,
    //  log-and-continue, throw an exception, whatever is wanted...
    std::cout
        << "sending error to my framework... ["
        << msg << "]\n";
    exit(0);
}
//  Prints:
//      sending error to my framework... [this is a test, this is only a test]