Exception handling in C++ is a well-unschooled topic if you observe initial stages of the learning curve. There are numerous tutorials available online on exception handling in C++. But few explains what you should not do & intricacies around it. So here I am to bridge the gap & show you some intricacies, from where & why you should not throw an exception and C++ exception handling best practices. Along with some newer features introduced for exception handling in Modern C++ with example.
In the end, we will see the performance cost of using an exception by a quick benchmark code. Finally, we will close the article with a summary of Best practices & some C++ Core Guidelines on exception handling.
Note: I would not cover anything regarding a dynamic exception as it deprecated from C++11 and removed in C++17.
Terminology/Jargon/Idiom You May Face
- potentially throwing: may or may not throw an exception.
- noexcept: this is specifier as well as operator depending upon where & how you use it. Will see that later.
- RAII: Resource Acquisition Is Initialization is a scope-bound resource management mechanism. Which means resource allocation done with the constructor & resource deallocation with the destructor during the defined scope of the object. I know it’s a terrible name but very powerful concept.
- Implicitly-declared special member functions: I think this need not require any introduction.
1. Implement Copy And/Or Move Constructor While Throwing User-Defined Type Object
- Upon throw expression, a copy of the exception object created as the original object goes out of the scope during the stack unwinding process.
- During that initialization, we may expect copy elision (see this) – omits copy or move constructors (object constructed directly into the storage of the target object).
- But even though copy elision may or may not apply you should provide proper copy constructor and/or move constructor which is what C++ standard mandates(see 15.1). See below compilation error for reference.
- Above error stands true till C++14. Since C++17, If the thrown object is a prvalue, the copy/move elision is guaranteed.
- If we catch an exception by value, we may also expect copy elision(compilers permitted to do so, but it is not mandatory). The exception object is an lvalue argument when initializing catch clause parameters.
TL;DR
class used for throwing the exception object needs copy and/or move constructors
2. Be Cautious While Throwing an Exception From the Constructor
- When an exception is thrown from a constructor, stack unwinding begins, destructors for the object will only be called, if an object creation is successful. So be caution with dynamic memory allocation here. In such cases, you should use RAII.
- As you can see in the above case, the destructor of
derive
is not executed, Because, it is not created successfully.
|
|
- In the case of constructor delegation, it is considered as the creation of object hence destructor of
derive
will be called.
TL;DR
When an exception is thrown from a constructor, destructors for the object will be called only & only if an object is created successfully
3. Avoid Throwing Exceptions out of a Destructor
- Above code seems straight forward but when you run it, it terminates as shown below rather than catching the exception. Reason for this is destructors are by default
noexcept
(i.e. non-throwing)
|
|
- `noexcept(false) will solve our problem as below
- But don’t do it. Destructors are by default non-throwing for a reason, and we must not throw exceptions in destructors unless we catch them inside the destructor.
Why you should not throw an exception from a destructor?
Because destructors are called during stack unwinding when an exception is thrown, and we are not allowed to throw another exception while the previous one is not caught – in such a case std::terminate
will be called.
- Consider the following example for more clarity.
- An exception will be thrown when the object
d
will be destroyed as a result of RAII. But at the same time destructor ofbase
will also be called as it is sub-object ofderive
which will again throw an exception. Now we have two exceptions at the same time which is invalid scenario &std::terminate
will be called.
There are some type trait utilities like std::is_nothrow_destructible
, std::is_nothrow_constructible
, etc. from #include<type_traits>
by which you can check whether the special member functions are exception-safe or not.
TL;DR
1. Destructors are by default noexcept
(i.e. non-throwing).
2. You should not throw exception out of destructors because destructors are called during stack unwinding when an exception is thrown, and we are not allowed to throw another exception while the previous one is not caught – in such a case std::terminate
will be called.
4. Nested Exception Handling Best Practice With std::exception_ptr( C++11) Example
This is more of a demonstration rather the best practice of the nested exception scenario using std::exception_ptr
. Although you can simply use std::exception
without complicating things much but std::exception_ptr
will provide us with the leverage of handling exception out of try
/ catch
clause.
|
|
- Above example looks complicated at first, but once you have implemented nested exception handler(i.e.
print_nested_exception
). Then you only need to focus on throwing the exception usingstd::throw_with_nested
function.
- The main thing to focus here is
print_nested_exception
function in which we are rewinding nested exception usingstd::rethrow_exception
&std::exception_ptr
. std::exception_ptr
is a shared pointer like type though dereferencing it is undefined behaviour. It can hold nullptr or point to an exception object and can be constructed as:
- Once
std::exception_ptr
is created, we can use it to throw or re-throw exceptions by calling `std::rethrow_exception(exception_ptr) as we did above, which throws the pointed exception object.
TL;DR
1. std::exception_ptr
extends the lifetime of a pointed exception object beyond a catch clause.
2. We may use std::exception_ptr
to delay the handling of a current exception and transfer it to some other palaces. Though, practical usecase of std::exception_ptr
is between threads.
5. Use noexcept `Specifier` vs `Operator` Appropriately
- I think this is an oblivious concept among the other concepts of the C++ exceptions.
noexcept
specifier & operator came in C++11 to replace deprecated(removed from C++17) dynamic exception specification.
|
|
noexcept Specifier
I think this needs no introduction it does what its name suggests. So let’s quickly go through some pointers:
- Can use for normal functions, methods, lambda functions & function pointer.
- From C++17, function pointer with noexcept can not points to potentially throwing function.
- Finally, don’t use
noexcept
specifier for virtual functions in a base class/interface because it enforces restriction for all overrides. - Don’t use noexcept unless you really need it. “Specify it when it is useful and correct” - Google’s cppguide.
noexcept Operator & What Is It Use For?
- Added in C++11,
noexcept
operator takes an expression (not necessarily constant) and performs a compile-time check determining if that expression is non-throwing (noexcept
) or potentially throwing. - The result of such compile-time check can be used, for example, to add
noexcept
specifier to the same category, higher-level function `(noexcept(noexcept(expr))) or in if constexpr. - We can use noexcept operator to check if some class has noexcept constructor, noexcept copy constructor, noexcept move constructor, and so on as follows:
|
|
- You must be wondering why & how this information will be useful?
This is more useful when you are using library functions inside your function to suggest compiler that your function is throwing or non-throwing depending upon library implementation. - If you remove constructor, copy constructor & move constructor, it will print
true
reason being implicitly-declared special member functions are always non-throwing.
TL;DRnoexcept
specifier & operator are two different things. noexcept
operator performs a compile-time check & doesn’t evaluate the expression. While noexcept
specifier can take only constant expressions that evaluate to either true or false.
6. Move Exception-Safe with std::move_if_noexcept
|
|
- We can use
noexcept(T(std::declval<T>()
) to check ifT
’s move constructor exists and isnoexcept
in order to decide if we want to create an instance ofT
by moving another instance ofT
(usingstd::move
). - Alternatively, we can use
std::move_if_noexcept
, which usesnoexcept
operator and casts to either rvalue or lvalue. Such checks are used instd::vector
and other containers. - This will be useful while you are processing critical data which you don’t want to lose. For example, we have critical data received from the server that we do not want to lose it at any cost while processing. In such a case, we should use
std::move_if_noexcept
which will move ownership of critical data only and only if move constructor is exception-safe.
TL;DR
Move critical object safely with std::move_if_noexcept
7. Real Cost of C++ Exception Handling With Benchmark
Despite many benefits, most people still do not prefer to use exceptions due to its overhead. So let’s clear it out of the way:
|
|
- As you can see above,
with_exception
&without_exception
has only a single difference i.e. exception syntax. But none of them throws any exceptions. - While
throwing_exception
does the same task except it throws an exception of typestd::out_of_range
in the last iteration. - As you can see in below bar graph, the last bar is slightly high as compared to the previous two which shows the cost of throwing an exception.
- But the cost of using exception is zero here, as the previous two bars are identical.
- I am not considering the optimization here which is the separate case as it trims some of the assembly instructions completely. Also, implementation of compiler & ABI plays a crucial role. But still, it is far better than losing time by setting up a guard(`if(error) strategy) and explicitly checking for the presence of error everywhere.
- While in case of exception, the compiler generates a side table that maps any point that may throw an exception (program counter) to the list of handlers. When an exception is thrown, this list consults to pick the right handler (if any) and the stack unwound. See this for in-depth knowledge.
- By the way, I am using a quick benchmark & which internally uses Google Benchmark, if you want to explore more.
- First and foremost, remember that using
try
andcatch
doesn’t actually decrease performance unless an exception is thrown. - It’s “zero cost” exception handling; no instruction related to exception handling executes until one is thrown.
- But, at the same time, it contributes to the size of executable due to unwinding routines, which may be important to consider for embedded systems.
TL;DR
No instruction related to exception handling is executed until one is thrown so using try
/ catch
doesn’t actually decrease performance.
Best Practices & Some C++ Core Guidelines on Exception Handling
C++ Exception Handling Best Practices
Ideally, you should not throw an exception from the destructor, move constructor or swap like functions.
Prefer RAII idiom for the exception safety because in case of exception you might be left with
- data in an invalid state, i.e. data that cannot be further read & used;
- leaked resources such as memory, files, ids, or anything else that needs to be allocated and released;
- corrupted memory;
- broken invariants, e.g. size function returns more elements than actually held in a container.Avoid using raw new & delete. Use solutions from the standard library, e.g. std::unique_pointer,
std::make_unique
,std::fstream
,std::lock_guard
, etc.Moreover, it is useful to split your code into modifying and non-modifying parts, where only the non-modifying part can throw exceptions.
Never throw exceptions while owing some resource.
Some CPP Core Guidelines
- E.1: Develop an error-handling strategy early in a design
- E.3: Use exceptions for error handling only
- E.6: Use RAII to prevent leaks
- E.13: Never throw while being the direct owner of an object
- E.16: Destructors, deallocation, and swap must never fail
- E.17: Don’t try to catch every exception in every function
- E.18: Minimize the use of explicit try/catch
- 26: If you can’t throw exceptions, consider failing fast
- E.31: Properly order your catch-clauses