Cpp2 currently supports three kinds of contracts:
-
Preconditions and postconditions. A function declaration can include
pre(condition)
andpost(condition)
before the= /* function body */
. Before entering the function body, preconditions are fully evaluated and postconditions are captured (and performs their captures, 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. Inside a function body, writing
assert(condition)
assertion statements. Assertions are evaluated when control flow passes through them.
Notes:
-
condition
is an expression that evaluates to#!cpp true
or#!cpp false
. -
Optionally,
condition
may be followed by, "message"
, a message to include if a violation occurs. For example,pre(condition, "message")
. -
Optionally, a
<Group>
can be written inside<
>
angle brackets immediately before the(
, to designate that this test is part of the contract group namedGroup
. If a violation occurs,Group.report_violation()
will be called. For example,pre<Group>(condition)
.
For example:
insert_at: (container, where: int, val: int)
pre<Bounds>( 0 <= where <= vec.ssize(), "position (where)$ is outside 'val'" )
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 becausewhere
is#!cpp -1
, then#!cpp cpp2::Bounds.report_violation("position -1 is outside 'val'")
is called. -
The postcondition is part of the
Default
safety contract group. If the check fails, then#!cpp cpp2::Default.report_violation()
is called.
Contract groups are useful to enable or disable or set custom handlers independently for different groups of contracts. A contract group G
is just the name of an object that can be called with G.report_violation()
and G.report_violation(message)
, where message
is a * const char
C-style text string.
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, for convenience using cpp2::contract_group
which is a convenient group type:
GroupA: cpp2::contract_group = (); // a global group
func: () = {
GroupB: cpp2::contract_group = (); // a local group
GroupC := new<cpp2::contract_group>(); // a dynamically allocated group
// ...
assert<GroupA >( some && condition );
assert<GroupB >( another && condition );
assert<GroupC*>( 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 used in cppfront's own reflection API:
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")
= { /*...*/ }
// ...
}
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#!cpp * (* const char)
. -
.get_handler()
returns the current handler function pointer, or null if none is installed. -
.has_handler()
returns whether there is a current handler installed. -
.enforce(condition, message)
evaluatescondition
, and if it isfalse
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
for type safety checks. -
Bounds
for bounds safety checks. -
Null
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:
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
std::cout
<< "sending error to my framework... ["
<< msg << "]\n";
exit(0);
}