|
| 1 | +--- |
| 2 | +title: Error handling in `MGIS` |
| 3 | +author: Thomas Helfer |
| 4 | +date: 2025 |
| 5 | +lang: en-EN |
| 6 | +numbersections: true |
| 7 | +documentclass: article |
| 8 | +from: markdown+tex_math_single_backslash |
| 9 | +geometry: |
| 10 | + - margin=2cm |
| 11 | +papersize: a4 |
| 12 | +link-citations: true |
| 13 | +colorlinks: true |
| 14 | +figPrefixTemplate: "$$i$$" |
| 15 | +tabPrefixTemplate: "$$i$$" |
| 16 | +secPrefixTemplate: "$$i$$" |
| 17 | +eqnPrefixTemplate: "($$i$$)" |
| 18 | +bibliography: bibliography.bib |
| 19 | +--- |
| 20 | + |
| 21 | +This section describes the error handling strategy introduced in |
| 22 | +`MGIS` Version 3.1 which is meant to: |
| 23 | + |
| 24 | +1. ensure performances and compatibility with the HPC techniques used |
| 25 | + in the code (MPI currently and eventually OpenMP in the future). |
| 26 | +2. provide rich error messages to the end-user. |
| 27 | + |
| 28 | +The first point eliminates the use of exceptions in most cases (see |
| 29 | +below the special cases of constructors). |
| 30 | + |
| 31 | +Some other standard strategies provided by the standard have also been |
| 32 | +discarded, although their design have inspired the proposed solution. |
| 33 | +For example, `std::error_code` only allows |
| 34 | +to use predefined error messages that are not able to describe the |
| 35 | +context. |
| 36 | + |
| 37 | +A function or a method using this error handling strategy is |
| 38 | +recognizable as follows: |
| 39 | + |
| 40 | +- It must take a class derived from `AbstractErrorHandler&` as its first |
| 41 | + parameter. Two main classes, derived from `AbstractErrorHandler&`, are |
| 42 | + available: `ContractViolationHandler` and `Context`. Consequently, if |
| 43 | + a function has a `AbstractErrorHandler&` as its first parameter, the |
| 44 | + reader knows that it may fail, and that the return type of the |
| 45 | + function should be interpreted according to the rule below. |
| 46 | +- It returns a value that can be invalid. The most common return type is |
| 47 | + a boolean which indicates success (`true`) or failure (`false`). |
| 48 | +- It must be declared `noexcept`. |
| 49 | + |
| 50 | +# Returned value of functions (or methods) that may fail |
| 51 | + |
| 52 | +A function that may fail shall generally return either: |
| 53 | + |
| 54 | +- a boolean associated with the success of the function, i.e. if a |
| 55 | + function returns `true`, it succed. |
| 56 | +- an `std::optional` object holding the results if those results are |
| 57 | + stored on the stack. |
| 58 | +- an `std::unique_ptr` or an `std::shared_ptr` object holding if those |
| 59 | + results are stored on the heap. In this case, the failure of the |
| 60 | + function is indicated by the fact that the underlying pointer is null. |
| 61 | +- a class to which an `InvalidValue` object is implicitly convertible. |
| 62 | + |
| 63 | +> ** Note ** |
| 64 | +> |
| 65 | +> Those returned value shall be mandatory marked with the `[[nodiscard]]` |
| 66 | +> attribute. However, this conflicts in many case with the `MGIS_EXPORT` |
| 67 | +> attribute. This is a defect of `C++-20`. |
| 68 | +
|
| 69 | +# `ContractViolationHandler` and `Context` |
| 70 | + |
| 71 | +Both classses `ContractViolationHandler` and `Context` inherit from |
| 72 | +`AbstractErrorHandler`. |
| 73 | + |
| 74 | +## `ContractViolationHandler` |
| 75 | + |
| 76 | +`ContractViolationHandler` is designed to report a contract violation, |
| 77 | +i.e. errors that shall not occur by design. |
| 78 | + |
| 79 | +`ContractViolationHandler` can be used in a `constexpr` context, if no |
| 80 | +contrat violation is detected. If a contract violation is detected, a |
| 81 | +compile-time error is generated since `registerErrorMessage` is not |
| 82 | +`constexpr` |
| 83 | + |
| 84 | +### Reporting error to end-user |
| 85 | + |
| 86 | +The `ContractViolationHandler` class provides the `registerErrorMessage` |
| 87 | +method which accepts a `C`-string. |
| 88 | + |
| 89 | +The behaviour of this class is to call `std::abort` in case of contract |
| 90 | +violation. This behaviour can be changed by passing |
| 91 | +`-Denable-exceptions=ON` to `cmake` when compiling `MGIS`. |
| 92 | + |
| 93 | +## `Context` |
| 94 | + |
| 95 | +The `Context` class is used for standard error management. |
| 96 | + |
| 97 | +### Reporting error to end-user |
| 98 | + |
| 99 | +The `Context` class inherits from the `ErrorBacktrace` class, which has |
| 100 | +been designed to store error messages in a hierarchical way from the |
| 101 | +lowest level of the code up to the highest level function. The |
| 102 | +`ErrorBacktrace` is meant to be used through the `Context` object which |
| 103 | +is passed as the first argument to most functions. |
| 104 | + |
| 105 | +The `ErrorBacktrace` class, and the `Context` class mostly provides the |
| 106 | +`registerErrorMessage` method that can register an error in the form of: |
| 107 | + |
| 108 | +1. a `C`-string |
| 109 | +2. a `C++`-string |
| 110 | +3. a couple of a function allowing to return an error message from an |
| 111 | + integer value. This kind of function is provided by many HPC |
| 112 | + libraries. |
| 113 | + |
| 114 | +For convenience, the `registerErrorMessage` always returns an invalid |
| 115 | +value, i.e. a value that is convertible to any of the returned type |
| 116 | +described in the previous section. |
| 117 | + |
| 118 | +By default, the error messages are packed up to the moment when error(s) |
| 119 | +must be reported to the end-user. The error messages can then be |
| 120 | +retrieved by the `getErrorMessage` method (or `getRawErrorMessage`, see |
| 121 | +below). However, if `MGIS` is compiled with the flag |
| 122 | +`-Denable-exceptions=ON`, an exception is thrown instead using |
| 123 | +`mgis::raise`. |
| 124 | + |
| 125 | +### Source location |
| 126 | + |
| 127 | +In debug mode, the source location, as returned by the |
| 128 | +`std::source_location::current_location` method, is automatically added |
| 129 | +to the error message returned by the `getErrorMessage` method. |
| 130 | + |
| 131 | +Note that this feature is currently only supported by `gcc` compilers. |
| 132 | + |
| 133 | +In some cases, the source location is not meaningful. In this case, the |
| 134 | +`registerErrorMessageWithoutSourceLocation` method can be used. |
| 135 | + |
| 136 | +If the information about the source location are not wanted, the |
| 137 | +`getRawErrorMessage` method can be used. |
| 138 | + |
| 139 | +## Example of usage of the `registerErrorMessage` method |
| 140 | + |
| 141 | +The following code illustrates the usage of the `ErrorBacktrace`, |
| 142 | +through a `Context` object: |
| 143 | + |
| 144 | +~~~~{.cxx} |
| 145 | +void processNext(){}; |
| 146 | +
|
| 147 | +bool f3(Context &ctx) |
| 148 | +{ |
| 149 | + // for this example, the message is useless |
| 150 | + // In pratice, one shall report the cause of the error |
| 151 | + // and not expose details, like the function name. |
| 152 | + // |
| 153 | + // Examples: |
| 154 | + // |
| 155 | + // - "negative temperature detected" |
| 156 | + // - "non convergence of the nonlinear solver" |
| 157 | + // |
| 158 | + // Note that the function name, the source file and the |
| 159 | + // line number are automatically added in debug mode. |
| 160 | + return ctx.registerErrorMessage("invalid call"); |
| 161 | +} |
| 162 | +
|
| 163 | +bool f2(Context &ctx) |
| 164 | +{ |
| 165 | + if (!f3(ctx)) { |
| 166 | + // f2 fails, but we don't have any more information |
| 167 | + // to add for the end user (i.e. f3 is an internal |
| 168 | + // method and a message like `f3 failed` is not |
| 169 | + // meaningful), so we just return |
| 170 | + return false; |
| 171 | + } |
| 172 | + processNext(); |
| 173 | + return true; |
| 174 | +} |
| 175 | +
|
| 176 | +bool f1(Context &ctx) |
| 177 | +{ |
| 178 | + if (!f2(ctx)) { |
| 179 | + return e.registerErrorMessage("invalid call to f2"); |
| 180 | + } |
| 181 | + processNext(); |
| 182 | + return true; |
| 183 | +} |
| 184 | +~~~~ |
| 185 | + |
| 186 | +If the `f1` function is called, the |
| 187 | +following error message is generated in release mode: |
| 188 | + |
| 189 | +~~~~ bash |
| 190 | +invalid call to f2 |
| 191 | +* invalid call |
| 192 | +~~~~ |
| 193 | + |
| 194 | +In debug mode, the following message is generated: |
| 195 | + |
| 196 | +~~~~{.bash} |
| 197 | +/home/UserDir/tests/core/error_backtrace_handler/error_backtrace_handler_test.cpp:31: in function 'bool f1(mgis::ErrorBacktrace&)': invalid call to f2 |
| 198 | +* /home/UserDir/tests/core/error_backtrace_handler/error_backtrace_handler_test.cpp:14: in function 'bool f3(mgis::ErrorBacktrace&)': invalid call |
| 199 | +~~~~ |
| 200 | + |
| 201 | +### Warning about usage of `C`-strings |
| 202 | + |
| 203 | +Note that in the case of a `C`-string, the string is not copied and only |
| 204 | +the pointer is stored. The developer must then ensure that this string |
| 205 | +is not destroyed. As a rule of thumb, this string shall belong to the |
| 206 | +data section of the binary. |
| 207 | + |
| 208 | +### Warning about usage of `C++`-strings |
| 209 | + |
| 210 | +`C++`-strings are the best way to report context sensitive error message. |
| 211 | +However, to reduce code bloat, building a complex error message shall |
| 212 | +never be implemented in a template function: one shall create a |
| 213 | +dedicated non-template function implemented in a source file. |
| 214 | + |
| 215 | +# The special case of constructors |
| 216 | + |
| 217 | +Constructors don't return values. There are mostly two ways to handle |
| 218 | +failure in constructors: |
| 219 | + |
| 220 | +- exceptions |
| 221 | +- having a boolean data member stating if the object is valid (this |
| 222 | + strategy is used by the standard `iostream` library for instance). |
| 223 | + |
| 224 | +Here, we propose to use exceptions and to wrap constructors in dedicated |
| 225 | +functions. |
| 226 | + |
| 227 | +## The `raise` function |
| 228 | + |
| 229 | +The `raise` function is an utility function to throw exception in a safe |
| 230 | +way: building the exception is not done in the `throw` statement. This |
| 231 | +is required to avoid a potential undefined behaviour if the constructor |
| 232 | +of the exception throws. |
| 233 | + |
| 234 | +The type of the exception thrown is given by the first template argument |
| 235 | +of the `raise` function and defaults to `std::runtime_exception`. |
| 236 | + |
| 237 | +## The `construct` function |
| 238 | + |
| 239 | +The `construct` function calls the constructor of an object that may |
| 240 | +throw an exception and returns a `std::optional` object that holds the |
| 241 | +object if the call to the constructor did not throw an exception. |
| 242 | + |
| 243 | +If the constructor threw an exception, then the error message hold by |
| 244 | +the exception is registered in the instance of the `ErrorBacktrace` |
| 245 | +class which is passed as the first argument of the function and an empty |
| 246 | +optional object is returned. |
| 247 | + |
| 248 | +Aside from the first argument (a reference to an instance of the |
| 249 | +`ErrorBacktrace` class), all the other arguments are forwarded to the |
| 250 | +constructor of the object. |
| 251 | + |
| 252 | +### The `MGIS_CONSTRUCT` macro |
| 253 | + |
| 254 | +The `MGIS_CONSTRUCT` macro is a wrapper |
| 255 | +around the `construct` function which adds |
| 256 | +the current source location when required (typically in the `debug` |
| 257 | +mode). |
| 258 | + |
| 259 | +### The `MGIS_TRY_CONSTRUCT` macro |
| 260 | + |
| 261 | +A typical pattern of usage of the `construct` function (through the |
| 262 | +`MGIS_CONSTRUCT` macro) is to try to build an object and: |
| 263 | + |
| 264 | +1. to defer the result in case of success. |
| 265 | +2. to stop the execution of the current function in case of failure. |
| 266 | + |
| 267 | +Here is a typical example of this pattern: |
| 268 | + |
| 269 | +~~~~{.cxx} |
| 270 | +auto tmp_v = MGIS_CONSTRUCT(ObjectType, e, ...); |
| 271 | +if(!tmp_v.has_value()){ |
| 272 | + return false; |
| 273 | +} |
| 274 | +auto& v = *(tmp_v); |
| 275 | +~~~~ |
| 276 | + |
| 277 | +The `MGIS_TRY_CONSTRUCT` macro reduces |
| 278 | +this code as follows: |
| 279 | + |
| 280 | +~~~~{.cxx} |
| 281 | +MGIS_TRY_CONSTRUCT(ObjectType, v, e, ...); |
| 282 | +// Here you can work with variable v which is a reference |
| 283 | +// to the ObjectType built by the "construct" function |
| 284 | +~~~~ |
| 285 | + |
| 286 | +## The `make_unique` function |
| 287 | + |
| 288 | +The `make_unique` function tries to allocate an object on the heap and |
| 289 | +stores it in a `std::unique_ptr`. |
| 290 | + |
| 291 | +If an exception is thrown during the construction of the object, the |
| 292 | +error message held by the exception is registered in an instance of the |
| 293 | +`ErrorBacktrace` and an empty pointer is |
| 294 | +returned. |
| 295 | + |
| 296 | +### Helper macros for the `make_unique` function |
| 297 | + |
| 298 | +The `MGIS_MAKE_UNIQUE` and |
| 299 | +`MGIS_TRY_MAKE_UNIQUE` macros are similar |
| 300 | +to the `MGIS_CONSTRUCT` and |
| 301 | +`MGIS_TRY_CONSTRUCT` macros respectively. |
| 302 | + |
| 303 | +## The `make_unique_as` function |
| 304 | + |
| 305 | +The `make_unique_as` function is similar to the `make_unique` function |
| 306 | +except that the built object is stored in a `std::unique_ptr` of some |
| 307 | +base class. This method is useful in a polymorphic context. |
| 308 | + |
| 309 | +### Helper macros for the `make_unique_as` function |
| 310 | + |
| 311 | +The `MGIS_MAKE_UNIQUE_AS` and `MGIS_TRY_MAKE_UNIQUE_AS` macros are |
| 312 | +similar to the `MGIS_CONSTRUCT` and `MGIS_TRY_CONSTRUCT` macros |
| 313 | +respectively. |
| 314 | + |
| 315 | +## The `make_shared` function |
| 316 | + |
| 317 | +The `make_shared` function is similar to the `make_unique` function |
| 318 | +except that the result is stored in a `std::shared_ptr`. |
| 319 | + |
| 320 | +### Helper macros for the `make_shared` function |
| 321 | + |
| 322 | +The `MGIS_MAKE_SHARED` and `MGIS_TRY_MAKE_SHARED` macros are similar to |
| 323 | +the `MGIS_CONSTRUCT` and `MGIS_TRY_CONSTRUCT` macros respectively. |
| 324 | + |
| 325 | +## The `make_shared_as` function |
| 326 | + |
| 327 | +The `make_shared_as` function is similar to the `make_unique_as` function |
| 328 | +except that the result is stored in a `std::shared_ptr`. |
| 329 | + |
| 330 | +### Helper macros for the `make_shared_as` function |
| 331 | + |
| 332 | +The `MGIS_MAKE_SHARED_AS` and `MGIS_TRY_MAKE_SHARED_AS` macros are |
| 333 | +similar to the `MGIS_CONSTRUCT` and `MGIS_TRY_CONSTRUCT` macros |
| 334 | +respectively. |
| 335 | + |
| 336 | +# Interaction with an external library that may use exceptions |
| 337 | + |
| 338 | +## The `registerExceptionInErrorBacktrace` function |
| 339 | + |
| 340 | +Call to external libraries that relies on the usage of exceptions must |
| 341 | +be encapsulated in appropriate `try/catch` |
| 342 | +blocks as follows: |
| 343 | + |
| 344 | +~~~~{.cxx} |
| 345 | +try{ |
| 346 | + .... |
| 347 | +} catch(...) |
| 348 | + registerExceptionInErrorBacktrace(e); |
| 349 | +} |
| 350 | +~~~~ |
| 351 | + |
| 352 | +The `registerExceptionInErrorBacktrace` is a Lippincott-like helper |
| 353 | +function which translate exceptions derived from `std::exception` into |
| 354 | +error messages. |
| 355 | + |
| 356 | +If the external library to be used, used another exception hierarchy, |
| 357 | +then appropriate versions of this helper function shall be created. |
0 commit comments