Contracts - DryPerspective/C_Builder_Extras GitHub Wiki

The contracts header adds support for function contract assertions - a customisationable form of assertion to provide more information and more control than a traditional C-style assert statement. When an assertion does not evaluate as true, this is considered to be a contract violation. This triggers a particular violation handler which is able to react accordingly with information about the violation as well as its location in the source code.

The library defines four "policies" for contract evaluation - enforce, observe, quick_enforce and ignore, with enforce being the default option. The default behaviour of these is simple - enforce is intended to interrupt the program and treat a violation as a serious error and by default this will throw an exception; observe is intended to note the violation but allow the program to continue, and by default will create a log file containing these violations; and ignore ignores any violations and will not do anything in response to them. The quick_enforce semantic will bypass the handler and call std::terminate(), however unlike in P2900 it is not possible to have it bypass the contract violation generation mechanism fully. The user may define their own handler and respond to these violations in any way that they desire on all semantics other than quick_enforce.

As a part of this repo, this header was written with C++Builder in mind. This severely limits our options. We must support C++98 and the limited language features in that language standard in our interface if not our execution. Some decision decisions were suboptimal, but as a safety feature, ease-of-use is paramount to ensure that this facility does not become too onerous to use. Support is provided using a traditional dp::contract_assert function, which is in general the recommended tool. However, a contract violation is built on a dp::source_location, and source_location depends on compiler extensions to properly evaluate the name of the function in the context of the caller. On compilers which do not support valid extensions for this behaviour (notably Embarcadero C++Builder) a macro CONTRACT_ASSERT is provided which will evaluate the function name in place and will enjoy broader support.

When considering design, in short, there were four main criteria to meet:

  • Easy to use.
  • A consistent interface in both C++98 and C++17.
  • Possible to use in constant expressions (in C++17).
  • Assertions made in library code should obey the handler chosen by the library user.

As such, the design is simple - the user can set the current policy and current handler for assertions to obey. This handler is retrieved when a violation occurs and determines what needs to happen. Note that in C++17, access to these variables is free of data races but it is the responsibility of the user to prevent broader race conditions; in the unlikely event you wish to make several changes to the current policy and handler from across several threads. Also note that in C++17 a failed assertion within a constexpr context will unconditionally prevent compilation from succeeding - it is not possible to customise compile time behaviour of failed assertions.

The tools in this header are in namespace dp::contract.

List of Features

Header Features
policy An enum (enum class in C++17) which describes the four semantics - enforce, observe, quick_enforce, and ignore.
Note that in both language standard supported, these can be retrieved via dp::contract::enforce to prevent a breaking change across versions.
violation A class which represents a given contract violation, containing information about the violation as well as the location in the source code in which it happened.
violation_exception A custom exception for contract violations, which inherits from std::runtime_error. Thrown by the default handler's enforce semantic.
handler_t A typedef for a pointer to the handler function - void(*)(violation)
default_handler The default handler function called if the user does not replace it with their own.
default_enforce Contains the enforce behaviour of the default handler, to prevent the user from needing to reinvent the wheel in their own handler.
default_observe Contains the observe behaviour of the default handler, to prevent the user from needing to reinvent the wheel in their own handler.
default_message Given a violation, returns the "default" message - "Contract violation in function [function]: [message]"
default_message_us Borland systems only. Given a violation, returns the "default" message - "Contract violation in function [function]: [message]" as a UnicodeString
set_handler Sets the current violation handler, and returns the previous one.
get_handler Retrieves the current violation handler.
set_policy Sets the current violation policy, and return the previous one.
get_policy Retrieves the current violation policy.
contract_assert The function used to make a given assertion.
CONTRACT_ASSERT(...) Macro used to make assertions, intended for situations where the function might not meet all needs on older compilers.
DP_NO_CONTRACT_MACROS If defined before including the "contracts.h" header, will disable the CONTRACT_ASSERT macro.
contract::violation members
function() Returns a string containing the function name in which the violation occurred
message() Returns a string containing the "message". If no message was specified by the contract annotation, this contains the violated condition as a string.
file() Returns a string containing the file in which the violation occurred
line() Returns the line number on which the violation occurred

How to use

On the user end, the use of assertions is simple. We will use dp::contract_assert here, but thanks to a macro "overloading" system, the interface will be the same using the CONTRACT_ASSERT macro.

In the most basic use, making an assertion is as simple as:

#include "contracts.h"

int main(){
  dp::contract_assert(some_condition);
}

This will assert some_condition to be true, and follow the default handler and default policy (enforce) if it is not.

However, this is not all. There are two other overloads to contract_assert - dp::contract_assert(condition, message) and dp::contract_assert(condition, message, handler). As you can probably deduce, the message parameter allows you to attach a particular message to the assertion, and handler allows you to use a particular handler function for this particular assertion. Note that for the dp::contract_assert function, if a message is not specified it will be an empty string. For the CONTRACT_ASSERT macro, the message will instead be the stringified code of the condition.

Customisation Guide

Customisation of behaviour with contracts is simple. In the simplest case, the overall policy followed can be set using dp::contract::set_policy. This will allow you to determine whether failed assertions will result in an enforce, observe, or ignore. For more fine-grained control, a custom handler function can be written. The signature of a valid function is void()(violation). Should the user write a function with this signature they can have all assertions follow it via dp::contract::set_handler.

Sample Code

void my_handler(dp::contract::violation viol){
    const auto pol{dp::contract::get_policy()};

    if(pol == dp::contract::observe){
        dp::contract::default_observe(viol); //We could also have used ADL to shorten
    }
    else if(pol == dp::contract::enforce){
        std::cerr << "Contract violation in function " << viol.function();
        std::terminate();
    }
}
  

template<typename T>
class not_null{
    T* ptr;
public:
    constexpr not_null(T* in) : ptr{in} {
        dp::contract_assert(ptr != nullptr);
    }

   //...
};

int main(){
   static constexpr int base{1};
   constexpr not_null ptr{&base}; //Contract checked and passes

   constexpr not_null<int> ptr{nullptr}; //Compilation fails

   dp::contract::set_handler(my_handler);

   dp::contract_assert(is_even(base), "Value must be even"); //Compiles and terminates according to my_handler
   
}

FAQ

  • P2900 Contracts are not for error checking but enforcing invariants, so why does this throw an exception by default? This was the strong preference of the intended user this header was written for, and writing the tool such that it did not require additional boilerplate to be used in this way was deemed preferable.
⚠️ **GitHub.com Fallback** ⚠️