Hi all, today I am going to write about a
renowned topic, namely, exception handling. Exception is a means for indication
of a deviation from the normal execution flow of the program, though it can
always be expected and handled appropriately.
As soon as an exception is thrown something known as stack unwinding happens, i.e. all the local objects (not static and not extern) on the stack in the current stack frame are destroyed (destructors are called). If the throw statement is immediately in the try block, then only the objects declared in the block will be destroyed, otherwise, if throw is in a function body, first the destructors of all local object allocated on the stack in that function body are called, then only the ones in try block. After that the exception object is copied to be passed to a handler body and the execution continues from the “corresponding” handler. Where the copied object is kept is unspecified. If no handler is found for the exception, std::termintate() function is called from <exception> header. The same happens (std::terminate() call) if an exception is thrown while unwinding the stack. That's why it is not recommended to have unsafe code in destructors, because if a local object of that type is created its destructor will be called during stack unwinding and the second throw statement will bring to program termination. We will get back to std::terminate() soon, but now let’s understand what a “corresponding” handler is.
Usually, an exception handler not only specifies the type of the exception to be caught, but also introduces a variable which the exception object will be assigned to. This is done because exceptions as a rule contain information about what went wrong, why, or any other user-defined info, at least exceptions also are class objects, so anyone can define his/her own exception classes. So the handler looks like this:
To
indicate such a deviation we need to generate an exception. For this
purpose the throw keyword is used. It needs an object as an
argument which will represent the exception. This could be either an object of
a built-in or a user-defined type:
throw int();
throw MyClass();
As soon as throw statement is executed the
execution leaves the current scope and starts to seek a “corresponding” handler.
A handler is introduced by means of catch
keyword which also needs the type of the exception to be handled:
catch (int)
{
// .. handle the exceptional situation
}
catch (MyClass)
{
// .. handle the exceptional situation
}
To handle the exception with catch statements the dangerous code (a
piece of code that may throw an exception) should be placed in a try block:
try {
// dangerous code with possible exception
}
So the final scenario is as follows:
try {
// .. some code
throw objectOfMyClass;
// .. maybe some more code
}
catch (MyClass)
{
// .. handle the exceptional situation
}
All these things are to provide a little
understanding of what goes on. Now let’s get deeper and see everything in more
details. The try block usually does
not contain the throw statement
directly. Instead, it contains a function call and that function includes either
a throw statement or another function
call with throw statement. I.e., the throw statement can be nested any number
of levels.
As soon as an exception is thrown something known as stack unwinding happens, i.e. all the local objects (not static and not extern) on the stack in the current stack frame are destroyed (destructors are called). If the throw statement is immediately in the try block, then only the objects declared in the block will be destroyed, otherwise, if throw is in a function body, first the destructors of all local object allocated on the stack in that function body are called, then only the ones in try block. After that the exception object is copied to be passed to a handler body and the execution continues from the “corresponding” handler. Where the copied object is kept is unspecified. If no handler is found for the exception, std::termintate() function is called from <exception> header. The same happens (std::terminate() call) if an exception is thrown while unwinding the stack. That's why it is not recommended to have unsafe code in destructors, because if a local object of that type is created its destructor will be called during stack unwinding and the second throw statement will bring to program termination. We will get back to std::terminate() soon, but now let’s understand what a “corresponding” handler is.
Usually, an exception handler not only specifies the type of the exception to be caught, but also introduces a variable which the exception object will be assigned to. This is done because exceptions as a rule contain information about what went wrong, why, or any other user-defined info, at least exceptions also are class objects, so anyone can define his/her own exception classes. So the handler looks like this:
catch (MyClass ex) {
// handle the exception or
// inform about failure in a friendly manner
}
Actually it is not a good practice to use the
type itself, as it will copy the exception object into the locally introduced
variable. So to avoid this overhead, we can catch the exception by reference
similar to passing arguments to functions. Here we go:
catch (const MyClass& ex)
{
// handle the exception or
// inform about failure in a friendly manner
}
The const
keyword may be omitted, but rarely one would want to modify the thrown
exception object. To catch an
exception one need not specify the exact type of the thrown object. The same
exception can be caught by a handler which expects an object of parent types,
for example:
try {
throw Derived();
}
catch (const Base& ex) {
// ... handle here
}
Note, that this is possible only in case of
public inheritance. If the Base is a
protected or a private base of Derived,
then the handler will not catch the exception. Now if we expect several types
of exceptions in a hierarchy we need to handle the most specific (derived) ones
first:
catch (const Derived& ex)
{
// exception of type Derived is handled here
}
catch (const Base& ex) {
// exception of type Base is handled here
}
If we change the order of handlers, then the
second one (expecting exceptions of type Derived)
will never be reached, as the first one will handle exceptions of both Base and Derived types. Modern compilers should at least warn about this.
As you see, the number of handlers is not
limited. We already know that the handlers with exact type or with base types
are “corresponding”. To learn about other possible correspondence, see the
clause 15.3.3 of C++ standard specification.
Exception handlers can also be nested. For example if one handler catches an exception of type T1 and throws another one of type T2, there could be a handler of type T2 inside the former one, as follows:
Exception handlers can also be nested. For example if one handler catches an exception of type T1 and throws another one of type T2, there could be a handler of type T2 inside the former one, as follows:
catch (const T1& ex) {
// try to handle T1
// if could not throw T2
try {
throw T2();
} catch (const T2&
ex) {
// handle T2 here
}
}
If we want to handle some exceptions which may raise from constructor body we can add try-catch blocks there. But if we want to handle the exceptions coming from the constructor initialization list, then the syntax changes a little. Here is what it looks like:
class MyClass : public Base
{
private:
SomeType
mem;
public:
MyClass(const SomeType& arg)
try : Base(), mem(arg)
{
// Any exception thrown from initialization list
// or from c'tor body can be handled below.
}
catch (...)
{
// A handler should follow the c'tor body.
}
};
It is possible, that a piece of code tries to
handle the exception, but if it does not manage to, it re-throws the exception, as if this piece of code never existed. Other handlers in different parts of system
may try to handle the exception and so on. In order to achieve this effect we again
refer to the same throw keyword, but
this time without any parameters:
catch (const T1& ex) {
// ... try to handle the exception
// if unable just rethrow
throw;
}
You might ask why don't we re-throw just throwing the caught
exception object (“throw ex;”)? Well, throw; throws the originally thrown
object, which means:
- it does not copy the exception object one more time as opposed to “throw ex;”
- it does not change the type of exception. What we caught is not necessarily what was thrown, so if we throw what we caught, the type of exception may be different. See the example below:
try {
throw Derived();
}
catch (const Base& ex) {
// The exception rethrown below is copied
// and it has type Base, not Derived
throw ex;
}
If we replace the last line in handler body with
“throw;” the original exception of type
Derived will be re-thrown.
The keyword throw
has another application either. It is exception
specification. This is a way to specify what types of exceptions may a
function throw:
void f1() throw(); // does not throw at all
void f2() throw(int); // may throw only int
void f3() throw(Base, int); // may throw Base and
int
If the function throws an exception of not
specified type, std::unexpected() is
called which by default calls std::terminate().
A little later about this as I already promised. We will not get much into the throw specifications as they are deprecated in C++11. For similar purpose
the noexcept keyword has been
introduced in C++11, which can be used either as an operator or a specifier.
You can find the details on the web, but here is a brief explanation.
- noexcept specifier specifies whether the function throws exceptions or not. It can be used with or without expression argument. If an expression is evaluated to true, then the function should not throw exceptions. Otherwise, it may. noexcept without expression argument is equivalent to noexcept(true). If a function marked with noexcept throws an exception, std::terminate() is called.
- noexcept operator is always used with a constant expression, and performs a check whether the expression is specified as noexcept. Note that this is a compile-time check.
The specifier and operator are often used together in
generic codes, for example:
template <class
Container>
int my_func(const
Container& c, int index) noexcept(noexcept(Container::operator[]))
{
return c[index];
}
If the Container’s
operator[] is marked with noexcept then the operator will return true, and my_func will also be noexcept,
otherwise it will not.
The time has come to talk about std::terminate() and std::unexpected(). As stated above std::unexpected() is called when a
function throws an exception inconsistent with its throw specification (remember
this is deprecated in C++11). By default std::unexpected()
in its turn calls std::terminate().
And std::terminate() by default
invokes std::abort() which stops the
program execution without calling destructors for any constructed object.
Nevertheless, we are free to substitute this functions with our owns. There are
two typedefs in <exception> header:
typedef void
(*unexpected_handler)();
typedef void
(*terminate_handler)();
These are just pointers to functions accepting
no arguments and returning nothing. And there are two functions which make it
possible to override what std::unexpected()
and std::terminate() call with our own functions:
unexpected_handler
set_unexpected(unexpected_handler f) throw();
terminate_handler set_terminate (terminate_handler
f) throw();
In C++11 both functions are marked with noexcept specifier instead of empty
exception specification. Both functions return the currently set handlers. This
is good for keeping the current handlers to temporarily substitute them with
our owns and then restore again. Though in C++11 it is possible just to call std::get_unexpected() or std::get_terminate() to retrieve current
handlers.
There are many exception classes in standard
library <exception> header. The
most general one is std::exception
itself. And all the others derive from it. It has one public virtual function,
which returns a human-readable message about the exception:
virtual const char* what() const;
So you can call what() on any exception from standard library. There is also a set of predefined exception classes in <stdexcept> header.
That’s it. There are many things that you’ll
learn experimenting with exceptions. I cannot remember and write here all the
things, but I hope this post gives a good understanding of how the exception
handling mechanism works and how you can modify it. Thanks for your precious time.
Thanks for the useful information you have shared in this post.
ReplyDeleteI have a question related to stack unwinding. What will happen if another exception will be thrown during stack unwinding?
Thanks in advance.
I've been told that I had missed this part, so I've added just now before seeing your comment. The answer is: std::terminate() is called.
DeleteOk, I see. Thanks again.
Delete