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++ with example. But few explains what you should not do & intricacies around it. So here, we will see some intricacies, from where & why you should not throw an exception along with some newer features introduced in Modern C++ on exception handling with example. I am not an expert but this is what I have gained from various sources, courses & industry experiences.
/!\: Originally published @ www.vishalchovatiya.com.
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 Best practices & some CPP Core Guidelines on exception handling.
Note: we will not see anything regarding a dynaic exception as it deprecated from C++11 and removed in C++17.
struct demo
{
demo() = default;
demo(demo &&) = delete;
demo(const demo &) = delete;
};
int main()
{
throw demo{};
return 0;
}
error: call to deleted constructor of 'demo'
throw demo{};
^~~~~~
note: 'demo' has been explicitly marked deleted here
demo(demo &&) = delete;
^
1 error generated.
compiler exit status 1
TL;DR
class used for throwing the exception object needs copy and/or move constructors
struct base
{
base(){cout<<"base\n";}
~base(){cout<<"~base\n";}
};
struct derive : base
{
derive(){cout<<"derive\n"; throw -1;}
~derive(){cout<<"~derive\n";}
};
int main()
{
try{
derive{};
}
catch (...){}
return 0;
}
base
derive
~base
struct base
{
base() { cout << "base\n"; }
~base() { cout << "~base\n"; }
};
struct derive : base
{
derive() = default;
derive(int) : derive{}
{
cout << "derive\n";
throw - 1;
}
~derive() { cout << "~derive\n"; }
};
int main()
{
try{
derive{0};
}
catch (...){}
return 0;
}
base
derive
~derive
~base
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
struct demo
{
~demo() { throw std::exception{}; }
};
int main()
{
try{
demo d;
}
catch (const std::exception &){}
return 0;
}
noexcept
(i.e. non-throwing)$ clang++-7 -o main main.cpp
warning: '~demo' has a non-throwing exception specification but can still
throw [-Wexceptions]
~demo() { throw std::exception{}; }
^
note: destructor has a implicit non-throwing exception specification
~demo() { throw std::exception{}; }
^
1 warning generated.
$
$ ./main
terminate called after throwing an instance of 'std::exception'
what(): std::exception
exited, aborted
noexcept(false)
will solve our problem as below.struct X
{
~X() noexcept(false) { throw std::exception{}; }
};
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.struct base
{
~base() noexcept(false) { throw 1; }
};
struct derive : base
{
~derive() noexcept(false) { throw 2; }
};
int main()
{
try{
derive d;
}
catch (...){ }
return 0;
}
std::terminate
will be called.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.int main()
{
cout << std::boolalpha << std::is_nothrow_destructible<std::string>::value << endl;
cout << std::boolalpha << std::is_nothrow_constructible<std::string>::value << endl;
return 0;
}
TL;DR
1. Destructors are by default(i.e. non-throwing).noexcept
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 casewill be called.std::terminate
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.void print_nested_exception(const std::exception_ptr &eptr=std::current_exception(), size_t level=0)
{
static auto get_nested = [](auto &e) -> std::exception_ptr {
try { return dynamic_cast<const std::nested_exception &>(e).nested_ptr(); }
catch (const std::bad_cast&) { return nullptr; }
};
try{
if (eptr) std::rethrow_exception(eptr);
}
catch (const std::exception &e){
std::cerr << std::string(level, ' ') << "exception: " << e.what() << '\n';
print_nested_exception(get_nested(e), level + 1);// rewind all nested exception
}
}
// -----------------------------------------------------------------------------------------------
void func2(){
try { throw std::runtime_error("TESTING NESTED EXCEPTION SUCCESS"); }
catch (...) { std::throw_with_nested(std::runtime_error("func2() failed")); }
}
void func1(){
try { func2(); }
catch (...) { std::throw_with_nested(std::runtime_error("func1() failed")); }
}
int main()
{
try { func1(); }
catch (const std::exception&) { print_nested_exception(); }
return 0;
}
// Will only work with C++14 or above
print_nested_exception
). Then you only need to focus on throwing the exception using std::throw_with_nested
function.exception: func1() failed
exception: func2() failed
exception: TESTING NESTED EXCEPTION SUCCESS
print_nested_exception
function in which we are rewinding nested exception using std::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:std::exception_ptr e1; // null
std::exception_ptr e2 = std::current_exception(); // null or a current exception
std::exception_ptr e3 = std::make_exception_ptr(std::exception{}); // std::exception
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.extends the lifetime of a pointed exception object beyond a catch clause.std::exception_ptr
2. We may useto delay the handling of a current exception and transfer it to some other palaces. Though, practical usecase ofstd::exception_ptr
is between threads.std::exception_ptr
void func() throw(std::exception); // dynamic excpetions, removed from C++17
void potentially_throwing(); // may throw
void non_throwing() noexcept; // "specifier" specifying non-throwing function
void print() {}
void (*func_ptr)() noexcept = print; // Not OK from C++17, `print()` should be noexcept too, works in C++11/14
void debug_deep() noexcept(false) {} // specifier specifying throw
void debug() noexcept(noexcept(debug_deep())) {} // specifier & operator, will follow exception rule of `debug_deep`
auto l_non_throwing = []() noexcept {}; // Yeah..! lambdas are also in party
noexcept specifier
I think this needs no introduction it does what its name suggests. So let’s quickly go through some pointers:
noexcept
specifier for virtual functions in a base class/interface because it enforces restriction for all overrides.noexcept operator & what is it used for?
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.noexcept
specifier to the same category, higher-level function (noexcept(noexcept(expr)))
or in if constexpr.noexcept
operator to check if some class has noexcept
constructor, noexcept copy constructor, noexcept move constructor, and so on as follows:class demo
{
public:
demo() {}
demo(const demo &) {}
demo(demo &&) {}
void method() {}
};
int main()
{
cout << std::boolalpha << noexcept(demo()) << endl; // C
cout << std::boolalpha << noexcept(demo(demo())) << endl; // CC
cout << std::boolalpha << noexcept(demo(std::declval<demo>())) << endl; // MC
cout << std::boolalpha << noexcept(std::declval<demo>().method()) << endl; // Methods
}
// std::declval<T> returns an rvalue reference to a type
TL;DRspecifier & operator are two different things.noexcept
operator performs a compile-time check & doesn’t evaluate the expression. Whilenoexcept
specifier can take only constant expressions that evaluate to either true or false.noexcept
std::move_if_noexcept
struct demo
{
demo() = default;
demo(const demo &) { cout << "Copying\n"; }
// Exception safe move constructor
demo(demo &&) noexcept { cout << "Moving\n"; }
private:
std::vector<int> m_v;
};
int main()
{
demo obj1;
if (noexcept(demo(std::declval<demo>()))){ // if moving safe
demo obj2(std::move(obj1)); // then move it
}
else{
demo obj2(obj1); // otherwise copy it
}
demo obj3(std::move_if_noexcept(obj1)); // Alternatively you can do this----------------
return 0;
}
noexcept(T(std::declval<T>()))
to check if T
’s move constructor exists and is noexcept
in order to decide if we want to create an instance of T
by moving another instance of T (using std::move
).std::move_if_noexcept
, which uses noexcept
operator and casts to either rvalue or lvalue. Such checks are used in std::vector
and other containers.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 withstd::move_if_noexcept
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:
static void without_exception(benchmark::State &state){
for (auto _ : state){
std::vector<uint32_t> v(10000);
for (uint32_t i = 0; i < 10000; i++) v.at(i) = i;
}
}
BENCHMARK(without_exception);//----------------------------------------
static void with_exception(benchmark::State &state){
for (auto _ : state){
std::vector<uint32_t> v(10000);
for (uint32_t i = 0; i < 10000; i++){
try{
v.at(i) = i;
}
catch (const std::out_of_range &oor){}
}
}
}
BENCHMARK(with_exception);//--------------------------------------------
static void throwing_exception(benchmark::State &state){
for (auto _ : state){
std::vector<uint32_t> v(10000);
for (uint32_t i = 1; i < 10001; i++){
try{
v.at(i) = i;
}
catch (const std::out_of_range &oor){}
}
}
}
BENCHMARK(throwing_exception);//-----------------------------------------
with_exception
& without_exception
has only a single difference i.e. exception syntax. But none of them throws any exceptions.throwing_exception
does the same task except it throws an exception of type std::out_of_range
in the last iteration.if(error)
strategy) and explicitly checking for the presence of error everywhere.TL;DR
No instruction related to exception handling is executed until one is thrown so using/try
doesn’t actually decrease performance.catch
Best practices for C++ exception handling
std::unique_pointer
, std::make_unique
, std::fstream
, std::lock_guard
, etc.Some CPP Core Guidelines