MGISContractViolationHandler
and Context
This section describes the error handling strategy introduced in
MGIS Version 3.1 which is meant to:
The first point eliminates the use of exceptions in most cases (see below the special cases of constructors).
Some other standard strategies provided by the standard have also
been discarded, although their design have inspired the proposed
solution. For example, std::error_code only allows to use
predefined error messages that are not able to describe the context.
A function or a method using this error handling strategy is recognizable as follows:
AbstractErrorHandler& as its first parameter. Two main
classes, derived from AbstractErrorHandler&, are
available: ContractViolationHandler and
Context. Consequently, if a function has a
AbstractErrorHandler& as its first parameter, the
reader knows that it may fail, and that the return type of the function
should be interpreted according to the rule below.true) or failure
(false).noexcept.A function that may fail shall generally return either:
true, it succed.std::optional object holding the results if those
results are stored on the stack.std::unique_ptr or an std::shared_ptr
object holding if those results are stored on the heap. In this case,
the failure of the function is indicated by the fact that the underlying
pointer is null.InvalidValue object is implicitly
convertible.** Note **
Those returned value shall be mandatory marked with the
[[nodiscard]]attribute. However, this conflicts in many case with theMGIS_EXPORTattribute. This is a defect ofC++-20.
ContractViolationHandler
and ContextBoth classses ContractViolationHandler and
Context inherit from AbstractErrorHandler.
ContractViolationHandlerContractViolationHandler is designed to report a
contract violation, i.e. errors that shall not occur by design.
ContractViolationHandler can be used in a
constexpr context, if no contrat violation is detected. If
a contract violation is detected, a compile-time error is generated
since registerErrorMessage is not
constexpr
The ContractViolationHandler class provides the
registerErrorMessage method which accepts a
C-string.
The behaviour of this class is to call std::abort in
case of contract violation. This behaviour can be changed by passing
-Denable-exceptions=ON to cmake when compiling
MGIS.
ContextThe Context class is used for standard error
management.
Context::assertOrTerminate checks if an assertion is
true. If not, Context::terminate is called.
Context::terminate allows to terminate the program. This
shall only be used when an internal error is detected, for instance when
an invariant of the code is not respected. This method calls the
terminal handler registred by Context::setTerminateHandler.
The default terminate handler prints the current error stack on
std::cerr and calls std::terminate.
The Context class inherits from the
ErrorBacktrace class, which has been designed to store
error messages in a hierarchical way from the lowest level of the code
up to the highest level function. The ErrorBacktrace is
meant to be used through the Context object which is passed
as the first argument to most functions.
The ErrorBacktrace class, and the Context
class mostly provides the registerErrorMessage method that
can register an error in the form of:
C-stringC++-stringFor convenience, the registerErrorMessage always returns
an invalid value, i.e. a value that is convertible to any of the
returned type described in the previous section.
By default, the error messages are packed up to the moment when
error(s) must be reported to the end-user. The error messages can then
be retrieved by the getErrorMessage method (or
getRawErrorMessage, see below). However, if
MGIS is compiled with the flag
-Denable-exceptions=ON, an exception is thrown instead
using mgis::raise.
In debug mode, the source location, as returned by the
std::source_location::current_location method, is
automatically added to the error message returned by the
getErrorMessage method.
Note that this feature is currently only supported by
gcc compilers.
In some cases, the source location is not meaningful. In this case,
the registerErrorMessageWithoutSourceLocation method can be
used.
If the information about the source location are not wanted, the
getRawErrorMessage method can be used.
registerErrorMessage methodThe following code illustrates the usage of the
ErrorBacktrace, through a Context object:
void processNext(){};
bool f3(Context &ctx)
{
// for this example, the message is useless
// In pratice, one shall report the cause of the error
// and not expose details, like the function name.
//
// Examples:
//
// - "negative temperature detected"
// - "non convergence of the nonlinear solver"
//
// Note that the function name, the source file and the
// line number are automatically added in debug mode.
return ctx.registerErrorMessage("invalid call");
}
bool f2(Context &ctx)
{
if (!f3(ctx)) {
// f2 fails, but we don't have any more information
// to add for the end user (i.e. f3 is an internal
// method and a message like `f3 failed` is not
// meaningful), so we just return
return false;
}
processNext();
return true;
}
bool f1(Context &ctx)
{
if (!f2(ctx)) {
return e.registerErrorMessage("invalid call to f2");
}
processNext();
return true;
}If the f1 function is called, the following error
message is generated in release mode:
invalid call to f2
* invalid callIn debug mode, the following message is generated:
/home/UserDir/tests/core/error_backtrace_handler/error_backtrace_handler_test.cpp:31: in function 'bool f1(mgis::ErrorBacktrace&)': invalid call to f2
* /home/UserDir/tests/core/error_backtrace_handler/error_backtrace_handler_test.cpp:14: in function 'bool f3(mgis::ErrorBacktrace&)': invalid callC-stringsNote that in the case of a C-string, the string is not
copied and only the pointer is stored. The developer must then ensure
that this string is not destroyed. As a rule of thumb, this string shall
belong to the data section of the binary.
C++-stringsC++-strings are the best way to report context sensitive
error message. However, to reduce code bloat, building a complex error
message shall never be implemented in a template function: one shall
create a dedicated non-template function implemented in a source
file.
Constructors don’t return values. There are mostly two ways to handle failure in constructors:
iostream library for
instance).Here, we propose to use exceptions and to wrap constructors in dedicated functions.
raise functionThe raise function is an utility function to throw
exception in a safe way: building the exception is not done in the
throw statement. This is required to avoid a potential
undefined behaviour if the constructor of the exception throws.
The type of the exception thrown is given by the first template
argument of the raise function and defaults to
std::runtime_exception.
construct functionThe construct function calls the constructor of an
object that may throw an exception and returns a
std::optional object that holds the object if the call to
the constructor did not throw an exception.
If the constructor threw an exception, then the error message hold by
the exception is registered in the instance of the
ErrorBacktrace class which is passed as the first argument
of the function and an empty optional object is returned.
Aside from the first argument (a reference to an instance of the
ErrorBacktrace class), all the other arguments are
forwarded to the constructor of the object.
MGIS_CONSTRUCT
macroThe MGIS_CONSTRUCT macro is a wrapper around the
construct function which adds the current source location
when required (typically in the debug mode).
MGIS_TRY_CONSTRUCT macroA typical pattern of usage of the construct function
(through the MGIS_CONSTRUCT macro) is to try to build an
object and:
Here is a typical example of this pattern:
auto tmp_v = MGIS_CONSTRUCT(ObjectType, e, ...);
if(!tmp_v.has_value()){
return false;
}
auto& v = *(tmp_v);The MGIS_TRY_CONSTRUCT macro reduces this code as
follows:
MGIS_TRY_CONSTRUCT(ObjectType, v, e, ...);
// Here you can work with variable v which is a reference
// to the ObjectType built by the "construct" functionmake_unique
functionThe make_unique function tries to allocate an object on
the heap and stores it in a std::unique_ptr.
If an exception is thrown during the construction of the object, the
error message held by the exception is registered in an instance of the
ErrorBacktrace and an empty pointer is returned.
make_unique functionThe MGIS_MAKE_UNIQUE and
MGIS_TRY_MAKE_UNIQUE macros are similar to the
MGIS_CONSTRUCT and MGIS_TRY_CONSTRUCT macros
respectively.
make_unique_as
functionThe make_unique_as function is similar to the
make_unique function except that the built object is stored
in a std::unique_ptr of some base class. This method is
useful in a polymorphic context.
make_unique_as functionThe MGIS_MAKE_UNIQUE_AS and
MGIS_TRY_MAKE_UNIQUE_AS macros are similar to the
MGIS_CONSTRUCT and MGIS_TRY_CONSTRUCT macros
respectively.
make_shared
functionThe make_shared function is similar to the
make_unique function except that the result is stored in a
std::shared_ptr.
make_shared functionThe MGIS_MAKE_SHARED and
MGIS_TRY_MAKE_SHARED macros are similar to the
MGIS_CONSTRUCT and MGIS_TRY_CONSTRUCT macros
respectively.
make_shared_as
functionThe make_shared_as function is similar to the
make_unique_as function except that the result is stored in
a std::shared_ptr.
make_shared_as functionThe MGIS_MAKE_SHARED_AS and
MGIS_TRY_MAKE_SHARED_AS macros are similar to the
MGIS_CONSTRUCT and MGIS_TRY_CONSTRUCT macros
respectively.
registerExceptionInErrorBacktrace functionCall to external libraries that relies on the usage of exceptions
must be encapsulated in appropriate try/catch blocks as
follows:
try{
....
} catch(...)
registerExceptionInErrorBacktrace(e);
}The registerExceptionInErrorBacktrace is a
Lippincott-like helper function which translate exceptions derived from
std::exception into error messages.
If the external library to be used, used another exception hierarchy, then appropriate versions of this helper function shall be created.