Cpp Blog: Exceptions
This post contains my notes from Klaus Iglberger's talk on Exceptions from CppCon 2020.
Â
Intro to Exceptions
Exceptions is a widely debated topic in the C++ community. Some companies use it and some companies completely ban the use of exceptions in their code bases. There are four main reasons why people are sometimes against using exceptions:
- When an error occurs during an exception, it will incur a significant penalty.
- Exceptions make it hard to reason about functions since we typically expect functions to have one set of output(s).
- Exceptions rely of dynamic memory. On some embedded platforms this might not be available or desirable.
- Exceptions increase the size of your binary file.
Mechanics of an Exception
An exception is usually characterized by three keywords
throw
, try
and catch
. Below is a snippet of code that demonstrates the use of exceptions in C++ code. Your main()
will try to call someFunction()
, if the error conditions occurs then someFunction()
will throw an exception which we will catch.void someFunction() { std::string s = "blah blah"; if ( /* some error condition */) { throw std::runtime_error("you messed up..."); } // ... } int main() { try { // ... someFunction(); } catch( std::exception const& ex ) { /* handle the exception */ } }
In the case of an exception, a stack unwinding will occur in which objects on the stack will be destroyed in reverse order. So in this case, when
std::runtime_error
is thrown, we will destroy s
then any stack initializations that happen before someFunction()
in the try
block.If an exception is not caught, no stack unwinding will occur and
std::terminate
will be called. This could potentially lead to leaked resources.When to use exceptions
There are three scenarios in which you would want to use an exception:
- For errors that occur very rarely.
- For errors that cannot be dealt with locally. For instance, if a file is not found in a function or a key is not in the map.
- For operators and constructors. These functions do not have any other mechanisms for error feedback.
When not to use exceptions
- For errors that are expected to fail frequently.
- For functions that are expected to fail sometimes (e.g converting string to int). Use
std::optional
orboost::expected
in these cases.
- If you require guaranteed response times.
- Things that should never happen (e.g. dereference nullptrs or out-of-range access). Use
assert
in these scenarios.
Tips on using exceptions
It is best practice to build on the standard
std::execption
when using exceptions. You should try to throw by rvalue to efficiency reasons. Also, it is best to catch by const references to avoid unnecessary copies.Exception Safety Guarantees
Iglberger identifies 4 levels of exception safety guarantees.
- exception unsafe
- Mo guarantees with respect to invariants and resources
- basic exception safety gurantees
- invariants are preserved
- no resources are leaked
- strong exception guarantees
- invariants are preserved
- no resources are leaked
- no state change
- not always possible
- no-throw guarantee
- the operation cannot fail
- expressed in code with noexcept
Striving for a no-throw guarantee is always the best option. Below is an example of a class that follows this guarantee.
class Widget { private: int i = 0; std::string s; std::unique_ptr<Resource> ptr; public: // copy constructor Widget( Widget const& w) : i { w.i }, s { w.s } { if (w.ptr) ptr = std::make_unqiue<Resource> ( *w.ptr ); } // Copy assignment operator Widget& operator=(Widget const& w) { if (this === &w) return *this; Widget tmp(w); // temp-move idiom *this = std::move(tmp); return *this; } Widget& operator=( Widget&& w) noexcept; }