Commit 9da9b895 authored by GILLES Sebastien's avatar GILLES Sebastien
Browse files

Error handling notebook: add more content, and clean-up.

parent 84d866dc
%% Cell type:markdown id: tags:
# [Getting started in C++](/) - [Useful concepts and STL](/notebooks/5-UsefulConceptsAndSTL/0-main.ipynb)
# [Getting started in C++](/) - [Useful concepts and STL](./0-main.ipynb)
%% Cell type:markdown id: tags:
* [Error handling](/notebooks/5-UsefulConceptsAndSTL/1-ErrorHandling.ipynb)
* [TP 14](/notebooks/5-UsefulConceptsAndSTL/1b-TP.ipynb)
* [RAII idiom](/notebooks/5-UsefulConceptsAndSTL/2-RAII.ipynb)
* [Containers](/notebooks/5-UsefulConceptsAndSTL/3-Containers.ipynb)
* [TP 15](/notebooks/5-UsefulConceptsAndSTL/3b-TP.ipynb)
* [Associative containers](/notebooks/5-UsefulConceptsAndSTL/4-AssociativeContainers.ipynb)
* [Move semantics](/notebooks/5-UsefulConceptsAndSTL/5-MoveSemantics.ipynb)
* [Smart pointers](/notebooks/5-UsefulConceptsAndSTL/6-SmartPointers.ipynb)
* [TP 16](/notebooks/5-UsefulConceptsAndSTL/6b-TP.ipynb)
* [Algorithms](/notebooks/5-UsefulConceptsAndSTL/7-Algorithms.ipynb)
* [Error handling](./1-ErrorHandling.ipynb)
* [TP 14](./1b-TP.ipynb)
* [RAII idiom](./2-RAII.ipynb)
* [Containers](./3-Containers.ipynb)
* [TP 15](./3b-TP.ipynb)
* [Associative containers](./4-AssociativeContainers.ipynb)
* [Move semantics](./5-MoveSemantics.ipynb)
* [Smart pointers](./6-SmartPointers.ipynb)
* [TP 16](./6b-TP.ipynb)
* [Algorithms](./7-Algorithms.ipynb)
%% Cell type:markdown id: tags:
© _CNRS 2016_ - _Inria 2018-2019_
© _CNRS 2016_ - _Inria 2018-2020_
_This notebook is an adaptation of a lecture prepared by David Chamont (CNRS) under the terms of the licence [Attribution-NonCommercial-ShareAlike 4.0 International (CC BY-NC-SA 4.0)](http://creativecommons.org/licenses/by-nc-sa/4.0/)_
_The present version has been written by Sébastien Gilles and Vincent Rouvreau (Inria)_
......
%% Cell type:markdown id: tags:
# [Getting started in C++](/) - [Useful concepts and STL](/notebooks/5-UsefulConceptsAndSTL/0-main.ipynb) - [Error handling](/notebooks/5-UsefulConceptsAndSTL/1-ErrorHandling.ipynb)
# [Getting started in C++](/) - [Useful concepts and STL](./0-main.ipynb) - [Error handling](./1-ErrorHandling.ipynb)
%% Cell type:markdown id: tags:
<h1>Table of contents<span class="tocSkip"></span></h1>
<div class="toc"><ul class="toc-item"><li><span><a href="#Introduction" data-toc-modified-id="Introduction-1">Introduction</a></span></li><li><span><a href="#Compiler-warnings-and-errors" data-toc-modified-id="Compiler-warnings-and-errors-2">Compiler warnings and errors</a></span></li><li><span><a href="#Assert" data-toc-modified-id="Assert-3">Assert</a></span></li><li><span><a href="#Exceptions" data-toc-modified-id="Exceptions-4">Exceptions</a></span><ul class="toc-item"><li><span><a href="#throw" data-toc-modified-id="throw-4.1"><code>throw</code></a></span></li><li><span><a href="#try/catch" data-toc-modified-id="try/catch-4.2"><code>try</code>/<code>catch</code></a></span></li><li><span><a href="#Good-practice:-use-as-much-as-possible-exceptions-that-derive-from-std::exception" data-toc-modified-id="Good-practice:-use-as-much-as-possible-exceptions-that-derive-from-std::exception-4.3">Good practice: use as much as possible exceptions that derive from <code>std::exception</code></a></span></li><li><span><a href="#Good-practice:-be-wary-of-a-forest-of-exception-classes" data-toc-modified-id="Good-practice:-be-wary-of-a-forest-of-exception-classes-4.4">Good practice: be wary of a forest of exception classes</a></span></li><li><span><a href="#noexcept" data-toc-modified-id="noexcept-4.5"><code>noexcept</code></a></span></li><li><span><a href="#Good-practice:-never-throw-an-exception-from-a-destructor" data-toc-modified-id="Good-practice:-never-throw-an-exception-from-a-destructor-4.6">Good practice: never throw an exception from a destructor</a></span></li><li><span><a href="#The-exception-class-I-use" data-toc-modified-id="The-exception-class-I-use-4.7">The exception class I use</a></span></li></ul></li><li><span><a href="#Error-codes" data-toc-modified-id="Error-codes-5">Error codes</a></span></li></ul></div>
<div class="toc"><ul class="toc-item"><li><span><a href="#Introduction" data-toc-modified-id="Introduction-1">Introduction</a></span></li><li><span><a href="#Compiler-warnings-and-errors" data-toc-modified-id="Compiler-warnings-and-errors-2">Compiler warnings and errors</a></span></li><li><span><a href="#Assert" data-toc-modified-id="Assert-3">Assert</a></span></li><li><span><a href="#Exceptions" data-toc-modified-id="Exceptions-4">Exceptions</a></span><ul class="toc-item"><li><span><a href="#throw" data-toc-modified-id="throw-4.1"><code>throw</code></a></span></li><li><span><a href="#try/catch" data-toc-modified-id="try/catch-4.2"><code>try</code>/<code>catch</code></a></span></li><li><span><a href="#Re-throw" data-toc-modified-id="Re-throw-4.3">Re-throw</a></span></li><li><span><a href="#Good-practice:-use-as-much-as-possible-exceptions-that-derive-from-std::exception" data-toc-modified-id="Good-practice:-use-as-much-as-possible-exceptions-that-derive-from-std::exception-4.4">Good practice: use as much as possible exceptions that derive from <code>std::exception</code></a></span></li><li><span><a href="#Good-practice:-be-wary-of-a-forest-of-exception-classes" data-toc-modified-id="Good-practice:-be-wary-of-a-forest-of-exception-classes-4.5">Good practice: be wary of a forest of exception classes</a></span></li><li><span><a href="#noexcept" data-toc-modified-id="noexcept-4.6"><code>noexcept</code></a></span></li><li><span><a href="#Good-practice:-never-throw-an-exception-from-a-destructor" data-toc-modified-id="Good-practice:-never-throw-an-exception-from-a-destructor-4.7">Good practice: never throw an exception from a destructor</a></span></li><li><span><a href="#The-exception-class-I-use" data-toc-modified-id="The-exception-class-I-use-4.8">The exception class I use</a></span></li></ul></li><li><span><a href="#Error-codes" data-toc-modified-id="Error-codes-5">Error codes</a></span><ul class="toc-item"><li><span><a href="#nodiscard" data-toc-modified-id="nodiscard-5.1">nodiscard</a></span></li></ul></li></ul></div>
%% Cell type:markdown id: tags:
## Introduction
It is very important of course to be able to track and manage as nicely as possible when something goes south in your code. We will see in this chapter the main ways to provide such insurance.
## Compiler warnings and errors
The first way to find out possible errors are during compilation time: you may ensure your code is correct by making its compilation fails if not. There are many ways to do so, even more so if templates are involved; here are few of them we have already seen:
* `static_assert` we saw in [template introduction](/notebooks/4-Templates/1-Intro.ipynb#static_assert)
* `static_assert` we saw in [template introduction](../4-Templates/1-Intro.ipynb#static_assert)
* Duck typing failure: if a template argument used somewhere doesn't comply with the expected API.
* Locality of reference: use heavily blocks so that a variable is freed as soon as possible. This way, you will avoid mistakes of using a variable that is in fact no longer up-to-date.
* Strive to make your code without any compiler warning: if there are even as less as 10 warnings, you might not see an eleventh that might sneak its way at some point. Activate as many warnings as possible for your compiler, and deactivate those unwanted with care (see [this notebook](/notebooks/6-InRealEnvironment/4-ThirdParty.ipynb) to see how to manage third party warnings.).
* Strive to make your code without any compiler warning: if there are even as less as 10 warnings, you might not see an eleventh that might sneak its way at some point. Activate as many type of warnings as possible for your compiler, and deactivate those unwanted with care (see [this notebook](../6-InRealEnvironment/4-ThirdParty.ipynb) to see how to manage third party warnings.).
## Assert
`assert` is a very handy tool that your code behaves exactly as expected. `assert` takes one argument; if this argument is resolved to `false` the code aborts with an error message:
%% Cell type:code id: tags:
``` C++17
#undef NDEBUG // Don't bother with this outside of Xeus-cling!
#include <cassert>
#include <iostream>
// THIS CODE WILL KILL Xeus-cling kernel!
{
double* ptr = nullptr;
assert(ptr != nullptr && "Pointer should be initialized first!");
//< See the `&&`trick above: you can't provide a message like in `static_assert`,
// but you may use a AND condition and a string to detail the issue
// (it works because the string is evaluated as `true`).
std::cout << *ptr << std::endl;
}
```
%% Cell type:markdown id: tags:
(here in Xeus-cling it breaks the kernel...)
The perk of `assert` is that it checks the condition is `true` *only in debug mode*!
So you may get extensive tests in debug mode that are ignored once your code has been thoroughly checked and is ready for production use.
So you may get extensive tests in debug mode that are ignored once your code has been thoroughly checked and is ready for production use (_debug_ and _release_ mode will be explained in a [later notebook](../6-InRealEnvironment/3-Compilers.ipynb#Debug-and-release-flags)).
The example above is a very useful use: before dereferencing a pointer check it is not `nullptr` (hence the good practice to always initialize a pointer to `nullptr`...)
In **release mode**, the macro `NDEBUG` should be defined and all the `assert` declarations will be ignored by the compiler.
I recommend to use `assert` extensively in your code:
* You're using a pointer? Check it is not nullptr.
* You get in a function a `std::vector` and you know it should be 3 elements long? Fire up an assert to check this...
* A `std::vector` is expected to be sorted a given way? Check it through an `assert`... (yes it's a O(n) operation, but if your contract is broken you need to know it!)
Of course, your debug mode will be much slower; but its role is anyway to make sure your code is correct, not to be the fastest possible (release mode is there for that!)
%% Cell type:markdown id: tags:
## Exceptions
%% Cell type:markdown id: tags:
Asserts are clearly a developer tool: they are there to signal something does not behave as intended and therefore that there is a bug somewhere...
However, they are clearly not appropriate to handle an error of your program end-user: for instance if he specifies an invalid input file, you do not want an abort which is moreover handled only in debug mode!
### `throw`
There is an **exception** mechanism that is appropriate to deal with this; this mechanism is activated with the keyword `throw`.
%% Cell type:code id: tags:
``` C++17
#include <iostream>
void FunctionThatExpectsSingleDigitNumber(int n)
{
if (n < -9)
throw -1;
if (n > 9)
throw 1;
std::cout << "Valid digit is " << n << std::endl;
}
```
%% Cell type:code id: tags:
``` C++17
{
FunctionThatExpectsSingleDigitNumber(5);
std::cout << "End" << std::endl;
}
```
%% Cell type:code id: tags:
``` C++17
{
FunctionThatExpectsSingleDigitNumber(15);
std::cout << "End" << std::endl;
}
```
%% Cell type:markdown id: tags:
As you can see, an exception provokes an early exit of the function: the lines after the exception is thrown are not run, and unless it is caught it will stop at the abortion of the program.
%% Cell type:markdown id: tags:
### `try`/`catch`
`throw` expects an object which might be intercepted by the `catch` command if the exception occurred in a `try` block; `catch` is followed by a block in which something may be attempted (or not!)
%% Cell type:code id: tags:
``` C++17
{
try
{
FunctionThatExpectsSingleDigitNumber(15);
}
catch(int n)
{
if (n == 1)
std::cout << "Error: value is bigger than 9!" << std::endl;
if (n == -1)
std::cout << "Error: value is less than -9!" << std::endl;
}
}
```
%% Cell type:markdown id: tags:
If the type doesn't match, the exception is not caught; if you want to be sure to catch everything you may use the `...` syntax. The drawback with this syntax is you can't use the thrown object information:
%% Cell type:code id: tags:
``` C++17
{
try
{
FunctionThatExpectsSingleDigitNumber(15);
}
catch(float n) // doesn't work!
catch(float n) // doesn't catch your `int` exception!
{
std::cout << "Float case: " << n << " was provided and is not an integer" << std::endl;
}
catch(...)
{
std::cout << "Gluttony case... but no object to manipulate to extract more information!" << std::endl;
}
}
```
%% Cell type:markdown id: tags:
### Re-throw
Once an exception has been caught by a `catch` block, it is considered to be handled; the code will therefore go on to what is immediately after the block. If you want to throw the exception again (for instance afterloggin a message) you may just type `throw`:
%% Cell type:code id: tags:
``` C++17
// No re-throw
{
try
{
FunctionThatExpectsSingleDigitNumber(15);
}
catch(int n)
{
std::cout << "Int case: " << n << " not a single digit number" << std::endl;
}
std::cout << "After catch" << std::endl;
}
```
%% Cell type:code id: tags:
``` C++17
// Rethrow
{
try
{
FunctionThatExpectsSingleDigitNumber(15);
}
catch(int n)
{
std::cout << "Int case: " << n << " not a single digit number" << std::endl;
throw; // `throw n` would have been correct as well but is not necessary
}
std::cout << "After catch" << std::endl;
}
```
%% Cell type:markdown id: tags:
### Good practice: use as much as possible exceptions that derive from `std::exception`
Using the _catch all_ case is not recommended in most cases... In fact even the `int`/`float` case is not that smart: it is better to use an object with information about why the exception was raised in the first place.
It is advised to use exception classes derived from the `std::exception` one; this way you provide a catch all without the drawback mentioned earlier. This class provides a virtual `what()` method which gives away more intel about the issue:
%% Cell type:code id: tags:
``` C++17
#include <exception>
struct TooSmallError : public std::exception
{
virtual const char* what() const noexcept override // we'll go back to `noexcept` later...
{
return "Value is less than -9!";
}
};
```
%% Cell type:code id: tags:
``` C++17
struct TooBigError : public std::exception
{
virtual const char* what() const noexcept override
{
return "Value is more than 9!";
}
};
```
%% Cell type:code id: tags:
``` C++17
#include <iostream>
void FunctionThatExpectsSingleDigitNumber2(int n)
{
if (n < -9)
throw TooSmallError();
if (n > 9)
throw TooBigError();
std::cout << "Valid digit is " << n << std::endl;
}
```
%% Cell type:code id: tags:
``` C++17
{
try
{
FunctionThatExpectsSingleDigitNumber2(15);
}
catch(const std::exception& e)
{
std::cout << "Properly caught: " << e.what() << std::endl;
}
}
```
%% Cell type:markdown id: tags:
The information is now in the exception object, which is much better...
Unfortunately, you are not always privy to the choice: if for instance you're using [Boost library](https://www.boost.org) the exception class they use don't inherit from `std::exception` (but some derived ones such as `boost::filesystem::error` do...). In this case, make sure to foresee to catch them with a dedicated block:
%% Cell type:code id: tags:
``` C++17
// Does not run in Xeus-cling!
try
{
...
}
catch(const std::exception& e)
{
...
}
catch(const boost::exception& e)
{
...
}
```
%% Cell type:markdown id: tags:
### Good practice: be wary of a forest of exception classes
At first sight, it might be tempting to provide a specific exception whenever you want to throw one: this way, you are able to catch only this one later on.
In practice, it's not necessarily such a good idea:
* When the code becomes huge, you (and even more importantly a new developer) may be lost in all the possible exceptions.
* It is rather time consuming: defining a specific exception means a bit of boilerplate to write, and those minutes might have been spent more efficiently, as...
* Most of the time, you don't even need the filtering capacity; in my code for instance if an exception is thrown it is 99 % of the time to be caught in the `main()` function to terminate properly the execution.
STL provides many derived class from `std::exception` which you might use directly or as base of your own class; see [cppreference](https://en.cppreference.com/w/cpp/error/exception) for more details. [OpenClassrooms](https://openclassrooms.com/fr/courses/1894236-programmez-avec-le-langage-c/1903837-gerez-des-erreurs-avec-les-exceptions) (which despite its name is in french) sorted out the go-to exceptions for lazy developers which cover most of the cases (don't get me wrong: laziness is often an asset for a software developer!):
* `std::domain_error`
* `std::invalid_argument`
* `std::length_error`
* `std::out_of_range`
* `std::logic_error`
* `std::range_error`
* `std::overflow_error`
* `std::underflow_error`
* `std::runtime_error`
with the latter being the default choice if no other fit your issue. Most of those classes provide a `std::string` argument in its constructor so that you may explain exactly what went wrong.
### `noexcept`
Exceptions are in fact very subtle to use; see for instance \cite{Sutter1999} and \cite{Sutter2002} that deal with them extensively (hence their title!).
In C++03, it was possible to specify a method or a function wouldn't throw, but the underlying mechanism with keywords `throw` and `nothrow` was such a mess many C++ gurus warned against using them.
In C++11, they tried to rationalize it and a new keyword to replace them was introduced: `noexcept`.
In short, if you have a method you're 100 % percent sure can't throw an exception, add this suffix and the compiler may optimize even further. However, do not put it if an exception can be thrown: it would result in a ugly runtime crash should an exception be raised there... (and up to now compilers are completely oblivious to that: no associated warning is displayed).
As you saw, in recent C++ `what()` is to be a `noexcept` method. It is therefore a bad idea to try to allocate there the string to be returned: allocation and string manipulation could lead to an exception from the STL functions used.
%% Cell type:markdown id: tags:
### Good practice: never throw an exception from a destructor
The explanation is quite subtle and explained in detail in item 8 of \cite{Meyers2005}; however just know you should never throw an exception there. If you need to deal with it, use something else (`std::abort` for instance).
The explanation is quite subtle and explained in detail in item 8 of \cite{Meyers2005}; however just know you should never throw an exception there. If you need to deal with an error there, use something else (`std::abort` for instance).
### The exception class I use
I provide in [appendix](/notebooks/7-Appendix/HomemadeException.ipynb) my own exception class (which of course derives from `std::exception`) which provides additionally:
I (Sébastien) provide in [appendix](/notebooks/7-Appendix/HomemadeException.ipynb) my own exception class (which of course derives from `std::exception`) which provides additionally:
* A constructor with a string, to avoid defining a verbosy dedicated exception class for each case.
* Better management of the string display, with an underlying `std::string` object.
* Information about the location from where the exception was thrown.
Vincent uses up the STL exceptions described in [previous section](#Good-practice:-be-wary-of-a-forest-of-exception-classes).
## Error codes
A quick word about C-style error management which you might find in use in some libraries: **error codes**.
The principe of the error codes is that your functions and methods should return an `int` which provides an indication of the success or not of the call; the eventual values sought are returned from reference. For instance:
%% Cell type:code id: tags:
``` C++17
#include <type_traits>
constexpr auto INVALID_TYPE = -1;
```
%% Cell type:code id: tags:
``` C++17
template<class T>
int AbsoluteValue(T value, T& result)
{
if constexpr (!std::is_arithmetic<T>())
return INVALID_TYPE;
else
{
if (value < 0)
result = -value;
else
result = value;
return EXIT_SUCCESS;
}
}
```
%% Cell type:markdown id: tags:
I don't like these error codes much, because:
* The result can't be naturally given in return value and must be provided in argument.
* You have to bookkeep the possible error codes somewhere, and a user must know this somewhere to go consult them if something happens.
* You have to bookkeep the possible error codes somewhere, and a user must know this somewhere to go consult them if something happens (usually a header file: see for instance one for [PETSc library](https://www.mcs.anl.gov/petsc/petsc-master/include/petscerror.h.html)).
* In the libraries that use them, more often than not some are not self descriptive and you have to figure out what the hell the issue is.
* And more importantly, this relies on the end-user thinking to check the error value:
%% Cell type:code id: tags:
``` C++17
#include <string>
{
std::string hello("Hello world");
std::string absolute_value = "";
AbsoluteValue(hello, absolute_value); // No compilation or runtime error (or even warning)!
}
```
%% Cell type:markdown id: tags:
### nodiscard
The latter point may however be mitigated since C++17 with the attribute [nodiscard](https://en.cppreference.com/w/cpp/language/attributes/nodiscard), which helps your compiler figure out the return value should have been checked.
%% Cell type:code id: tags:
``` C++17
template<class T>
[[nodiscard]] int AbsoluteValueNoDiscard(T value, T& result)
{
if constexpr (!std::is_arithmetic<T>())
return INVALID_TYPE;
else
{
if (value < 0)
result = -value;
else
result = value;
return EXIT_SUCCESS;
}
}
```
%% Cell type:code id: tags:
``` C++17
#include <string>
{
std::string hello("Hello world");
std::string absolute_value = "";
AbsoluteValueNoDiscard(hello, absolute_value); // Now there is a warning! But only available after C++ 17...
}
```
%% Cell type:markdown id: tags:
# References
[<a id="cit-Sutter1999" href="#call-Sutter1999">Sutter1999</a>] Herb Sutter, ``_Exceptional C++: 47 Engineering Puzzles, Programming Problems, and Solutions_'', 2000.
[<a id="cit-Sutter2002" href="#call-Sutter2002">Sutter2002</a>] Herb Sutter, ``_More Exceptional C++: 40 New Engineering Puzzles, Programming Problems, and Solutions_'', 2002.
[<a id="cit-Meyers2005" href="#call-Meyers2005">Meyers2005</a>] Scott Meyers, ``_Effective C++: 55 Specific Ways to Improve Your Programs and Designs (3rd Edition)_'', 2005.
%% Cell type:markdown id: tags:
© _CNRS 2016_ - _Inria 2018-2019_
© _CNRS 2016_ - _Inria 2018-2020_
_This notebook is an adaptation of a lecture prepared by David Chamont (CNRS) under the terms of the licence [Attribution-NonCommercial-ShareAlike 4.0 International (CC BY-NC-SA 4.0)](http://creativecommons.org/licenses/by-nc-sa/4.0/)_
_The present version has been written by Sébastien Gilles and Vincent Rouvreau (Inria)_
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment