diff --git a/CppCoreGuidelines.md b/CppCoreGuidelines.md index 112ced6..1c60633 100644 --- a/CppCoreGuidelines.md +++ b/CppCoreGuidelines.md @@ -10211,8 +10211,10 @@ Error-handling rule summary: * [E.18: Minimize the use of explicit `try`/`catch`](#Re-catch) * [E.19: Use a `final_action` object to express cleanup if no suitable resource handle is available](#Re-finally) -* [E.25: ??? What to do in programs where exceptions cannot be thrown](#Re-no-throw) -* ??? +* [E.25: If you can't throw exceptions, simulate RAII for resource management](Re-no-throw-raii) +* [E.26: If you can't throw exceptions, consider failing fast](#Re-no-throw-crash) +* [E.27: If you can't throw exceptions, use error codes systematically](#Re-no-throw-codes) +* [E.28: Avoid error handling based on global state (e.g. `errno`)](#Re-no-throw) ### E.1: Develop an error-handling strategy early in a design @@ -10279,6 +10281,10 @@ Don't use a `throw` as simply an alternative way of returning a value from a fun **See also**: [discussion](#Sd-noexcept) +##### Note + +Before deciding that you cannot afford or don't like exception-based error handling, have a look at the [alternatives](#Re-no-throw-raii). + ### E.3: Use exceptions for error handling only ##### Reason @@ -10677,20 +10683,48 @@ Let cleanup actions on the unwinding path be handled by [RAII](#Re-raii). ##### Reason `try`/`catch` is verbose and non-trivial uses error-prone. + `try`/`catch` can be a sign of unsystematic and/or low-level resource management or error handling. -##### Example +##### Example, Bad - ??? + void f(zstring s) + { + Gadget* p; + try { + p = new Gadget(s); + // ... + } + catch (Gadget_construction_failure) { + delete p; + throw; + } + } + +This code is messy. +There could be a leak from the naked pointer in the `try` block. +Not all exceptiones are handled. +`deleting` an object that failed to construct is almost certainly a mistake. +Better: + + void f2(zstring s) + { + Gadget g {s}; + } + +##### Alternatives + +* proper resource handles and [RAII](#Re-raii) +* [`finally`](#Re-finally) ##### Enforcement -??? +??? hard, needs a heuristic ### E.19: Use a `final_action` object to express cleanup if no suitable resource handle is available ##### Reason - `finally` is less verbose and harder to get wrong than `try`/`catch`. +`finally` is less verbose and harder to get wrong than `try`/`catch`. ##### Example @@ -10701,15 +10735,290 @@ Let cleanup actions on the unwinding path be handled by [RAII](#Re-raii). // ... } -**See also** ???? +##### Note -### E.25: ??? What to do in programs where exceptions cannot be thrown +`finally` is not as messy as `try`/`catch`, but it is still ad-hoc. +Prefer [proper resource management objects](#Re-raii). + +### E.25: If you can't throw exceptions, simulate RAII for resource management + +##### Reason + +Even without exceptions, [RAII](#Re-raii) is usually the best and most systematic way of dealing with resources. ##### Note -??? mostly, you can afford exceptions and code gets simpler with exceptions ??? +Error handling using exceptions is the only complete and systematic way of handling non-local errors in C++. +In particular, non-intrusively signalling failure to construct an object requires an exception. +Signalling errors in a way that cannot be ignored requires exceptions. +If you can't use exceptions, simulate their use as best you can. + +A lot of fear of exceptions is misguided. +When used for exceptional circumstances in code that is not littered with pointers and complicated control structures, +exception handling is almost always affordable (in time and space) and almost always leads to better code. +This, of course, assumes a good implementation of the exception handling mechanisms, which is not available on all systems. +There are also cases where the problems above do not apply, but exceptions cannot be used for other reasons. +Some hard real-time systems are an example: An operation has to be completed within a fixed time with an error or a correct answer. +In the absence of appropriate time estimation tools, this is hard to guarantee for exceptions. +Such systems (e.g. flight control software) typically also ban the use of dynamic (heap) memory. + +So, the primary guideline for error handling is "use exceptions and [RAII](#Re-raii)." +This section deals with the cases where you either do not have an efficient implementation or exceptions +or have such a rat's nest of old-style code +(e.g., lots of pointers, ill-defined ownership, and lots of unsystematic error handling based on tests of errors codes) +that it is infeasible to introduce simple and systematic exception handling. + +Before condemning exceptions or complaining too much about their cost, consider examples of the use of [error codes](#Re-no-throw-codes). + +##### Example + +Assume you wanted to write + + void func(int n) + { + Gadget g(n); + // ... + } + +If the `gadget` isn't correctly constructed, `func` exits with an exception. +If we cannot throw an exception, we can simulate this RAII style of resource handling by adding a `valid()` member function to `Gadget`: + + error_indicator func(int n) + { + Gadget g(n); + if (!g.valid()) return gadget_construction_error; + // ... + return 0; // zero indicates "good" + } + +The problem is of course that the caller now have to remember to test the return value. + **See also**: [Discussion](#Sd-???). +##### Enforcement + +Possible (only) for specific versions of this idea: e.g., test for systematic test of `valid()` after resource handle construction + + +## E.26: If you can't throw exceptions, consider failing fast + +##### Reason + +If you can't do a good job at recovering, at least you can get out before too much consequential damage is done. + +See also [Simulating RAII](#Re-no-throw-raii). + +##### Note + +If you cannot be systematic about error handling, consider "crashing" as a response to any error that cannot be handled locally. +That is, if you cannot recover from an error in the context of the function that detected it, call `abort()`, `quick_exit()`, +or a similar function that will trigger some sort of system restart. + +In systems where you have lots of processes and/or lots of computers, you need to expect and handle fatal crashes anyway, +say from hardware failures. +In such cases, "crashing" is simply leaving error handling to the next level of the system. + +##### Example + + void do_something(int n) + { + // ... + p = static_cast(malloc(n,X)); + if (p==nullptr) abort(); // abort if memory is exhausted + // ... + } + +Most systems cannot handle memory exhaustion gracefully anyway. This is roughly equivalent to + + void do_something(Int n) + { + // ... + p = new X[n]; // throw if memory is exhausted (by default, terminate) + // ... + } + +Typically, it is a good idea to log the reason for the "crash" before exiting. + +##### Enforcement + +Awkward + +## E.27: If you can't throw exceptions, use error codes systematically + +##### Reason + +Systematic use of any error-handling strategy minimizes the chance of forgetting to handle an error. + +See also [Simulating RAII](#Re-no-throw-raii). + +##### Note + +There are several issues to be addressed: + +* how do you transmit an error indicator from out of a function? +* how do you release all resources from a function before doing an error exit? +* What do you use as an error indicator? + +In general, returning an error indicator implies returning two values: The result and an error indicator. +The error indicator can be part of the object, e.g. an object can have a `valid()` indicator +or a pair of values can be returned. + +##### Example + + Gadget make_gadget(int n) + { + // ... + } + + void user() + { + Gadget g = make_gadget(17); + if (!g.valid()) { + // error handling + } + // ... + } + +This approach fits with [simulated RAII resource management](#Re-no-throw-raii). +The `valid()` function could return an `error_indicator` (e.g. a member of an `error_indicator` enumeration). + +##### Example + +What if we cannot or do not want to modify the `Gadget` type? +In that case, we must return a pair of values. +For example: + + std::pair make_gadget(int n) + { + // ... + } + + void user() + { + auto r = make_gadget(17); + if (!r.second) { + // error handling + } + Gadget& g = r.first; + // ... + } + +As shown, `std::pair` is a possible return type. +Some people prefer a specific type. +For example: + + Gval make_gadget(int n) + { + // ... + } + + void user() + { + auto r = make_gadget(17); + if (!r.err) { + // error handling + } + Gadget& g = r.val; + // ... + } + +One reason to prefer a specific return type is to have names for its members, rather than the somewhat cryptic `first` and `second` +and to avoid confusion with other uses of `std::pair`. + +###### Example + +In general, you must clean up before an eror exit. +This can be messy: + + std::pair user() + { + Gadget g1 = make_gadget(17); + if (!g1.valid()) { + return {0,g1_error}; + } + + Gadget g2 = make_gadget(17); + if (!g2.valid()) { + cleanup(g1); + return {0,g2_error}; + } + + // ... + + if (all_foobar(g1,g2)) { + cleanup(g1); + cleanup(g2); + return {0,foobar_error}; + // ... + + cleanup(g1); + cleanup(g2); + return {res,0}; + } + +Simulating RAII can be non-trivial, especially in functions with multiple resources and multiple possible errors. +A not uncommon technique is to gather cleanup at the end of the function to avoid repetittion: + + std::pair user() + { + error_indicator err = 0; + + Gadget g1 = make_gadget(17); + if (!g1.valid()) { + err = g2_error; + goto exit; + } + + Gadget g2 = make_gadget(17); + if (!g2.valid()) { + err = g2_error; + goto exit; + } + + if (all_foobar(g1,g2)) { + err = foobar_error; + goto exit; + } + // ... + exit: + if (g1.valid()) cleanup(g1); + if (g1.valid()) cleanup(g2); + return {res,err}; + } + +The larger the function, the more tempting this technique becomes. +Aso, the larger the program becomes the harder it is to apply an error-indicator-based error handling strategy systematically. + +We [prefer exception-based error handling](#Re-throw) and recommend [keeping functions short](#Rf-single). + +**See also**: [Discussion](#Sd-???). + +##### Enforcement + +Awkward. + +## E.28: Avoid error handling based on global state (e.g. `errno`) + +##### Reason + +Global state is hard to manage and it is easy to forget to check it. +When did you last test the return value of `printf()`? + +See also [Simulating RAII](#Re-no-throw-raii). + +##### Example, bad + + ??? + +##### Note + +C-stye error handling is based on the global variable `errno`, so it is essentially impossible to avoid this style completely. + +##### Enforcement + +Awkward. + + # Con: Constants and Immutability You can't have a race condition on a constant.