02 - catching

Multiple catch blocks

try and catch blocks always have to be together, but the catch block can appear multiple times.

If no match is found, exception continues to propagate and unwind the stack untill another try-catch block is found upwards the call stack.

If a match is found, control flow enters the scope of a specific handler. Because exception objects are polymorphic and they are rarely modified, they are typically caught by const reference - this prevents object slicing and ensures const-correctness.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#include <iostream>
#include <exception>

class a_exception : public std::exception {};
class b_exception : public a_exception {};
class c_exception : public b_exception {};
class d_exception : public c_exception {};

void func()
{
	// throw what you like here
}

int main()
{
	std::cout << "program start\n";

	try {
		std::cout << "before func()\n";
		func();
		std::cout << "after func()\n"; // if func() throws this is never executed
	}
	catch (const d_exception& e) {
		std::cout << "caught d exception\n";
	}
	catch (const c_exception& e) {
		std::cout << "caught c exception\n";
	}
	catch (const b_exception& e) {
		std::cout << "caught b exception\n";
	}
	catch (const a_exception& e) {
		std::cout << "caught a exception\n";
	}
	catch (const std::exception& e) {
		std::cout << "caught standard exception\n";
	}
	catch (...) {
		std::cout << "caught unknown exception\n";
	}

	std::cout << "program end\n";
}

Suppose func() throws b_exception, then:

  • first catch block is ignored because b_exception is not (a child of) d_exception

  • second catch block is ignored because b_exception is not (a child of) c_exception

  • third catch block is matched (types are identical); it is executed

  • program continues past last catch block

Effectively, the program prints:

program start
before func()
caught b exception
program end

If the order of handlers was different, exceptions objects could be caught with unnecessary loss of type information.

catch (...)

catch (...) will catch every exception regardless of it's type (it might not be even a class, just whatever that was thrown)*.

The good is that it catches everything. The bad is that you don't know the type of caught exception and have no way to use it.

Obviously catch-all block should be always the last. No handler after this one could ever be matched.

*: Microsoft compiler offers a language extension where some UB (such as null pointer dereference) is turned into exceptions. Such exceptions are beyond official C++ and require implementation-specific __try and __except keywords.

Catching multiple exceptions

Some languages offer a feature to catch exceptions of multiple, different dynamic types in one handler block. C++ does not - you just can't have multi-argument catch clause.

There are 2 solutions for it:

  • catching each separately and invoking same code as the handler

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <stdexcept>
#include <optional>
#include <functional>

int may_throw();

int func()
{
	auto handler = [](const std::exception& /* e */) {
		/* do stuff with e */
		return 0;
	};

	try {
		return may_throw();
	}
	catch (const std::runtime_error& e) {
		return handler(e);
	}
	catch (const std::bad_function_call& e) {
		return handler(e);
	}
	catch (const std::bad_optional_access& e) {
		return handler(e);
	}

	// This function produces no control flow warnings!
	// Any code here is unreachable - func either:
	// - executes return from the try block
	// - returns from a handler
	// - exits via uncaught exception
}
  • catching a generic type and performing dynamic casts to check dynamic type

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <stdexcept>
#include <optional>
#include <functional>

int may_throw();

int func()
{
	try {
		return may_throw();
	}
	catch (const std::exception& e) {
		if (auto ptr = dynamic_cast<const std::runtime_error*>(&e); ptr != nullptr) {
			// handle runtime_error
			return 0;
		}
		else if (auto ptr = dynamic_cast<const std::bad_function_call*>(&e); ptr != nullptr) {
			// handle bad_function_call
			return 0;
		}
		else if (auto ptr = dynamic_cast<const std::bad_optional_access*>(&e); ptr != nullptr) {
			// handle bad_optional_access
			return 0;
		}
		else {
			throw; // rethrow exception
		}
	}

	// (unreachable code)
}

Comparing by machine code, they will be very similar. Both dynamic_cast and built-in catch mechanism relies on RTTI so both implementations perform roughly the same operations. For the latter version, if the exception is not matched it can simply be thrown again. Because of this and additional conditional instructions I presume most programmers would think of the first approach as cleaner.

Rethrowing

Caught exceptions can be thrown again, simply by writing throw;. The statement doesn't have to be directly within a catch block, it can be executed indirectly, e.g. from a function which is called inside the handler. No new objects are created, the current exception simply continues to propagate up the stack; any handlers immediately after the one which rethrows are not checked. One can also write regular throw statement (with an argument) to throw a new exception object.

If throw; is executed while there is no exception object, std::terminate is called.

Rethrowing and throwing again (an exception of a different type) is useful in some minor cases such as:

  • Capturing a narrow set of errors and rethrowing them as something different (better understood by higher-level code).

  • Logging intermediate state (that would be potentially lost if caught as a base class).

  • Performing partial cleanup (e.g. closing the connection), then continuing to propagate failure about network operation.

Remember that the caught object might be of a more derived type than actually visible. throw; simply continues propagation of the current exception, whatever is is. It may also result in better diagnostics from some tools: correct rethrowing ensures continuity of the stack unwinding process, throwing another object may reset/discard exception call stack information.

Additional utilities

The standard library header <exception> offers some additional (magic) utility functions.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
namespace std {

// A special smart pointer type capable of referencing an exception object.
// Can not be used as or converted to a raw pointer, but special
// standard library functions accept exception_ptr as an argument.
class exception_ptr {
public:
	// creates empty exception_ptr
	exception_ptr() noexcept;
	// the class is copyable, copies work like std::shared_ptr
	exception_ptr(const exception_ptr& other) noexcept;

	// check if non-empty
	explicit operator bool() const noexcept;

private:
	/* compiler magic */
};

bool operator==(const exception_ptr& lhs, const exception_ptr& rhs) noexcept;
bool operator!=(const exception_ptr& lhs, const exception_ptr& rhs) noexcept;

// Return a smart pointer to the current exception
// - if no exception is propagating, returns empty exception_ptr
// - otherwise stops propagation and returns non-empty exception_ptr
// - in case of problems (e.g. no memory) may return non-empty
//   exception_ptr to std::bad_alloc or std::bad_exception
exception_ptr current_exception() noexcept;

// Throw a given object and make exception_ptr for it.
// The parameter is passed by value and thus may be sliced
// (make sure you have the local object, not a reference to its base).
template <typename E>
exception_ptr make_exception_ptr(E e) noexcept
{
	// This isn't the actual implementation, but it works as such.
	try {
		throw e;
	} catch (...) {
		return current_exception();
	}
}

// p must not be empty, otherwise undefined behavior.
// The exception object may be copied for implementation reasons.
[[noreturn]] void rethrow_exception(exception_ptr p);

// ... even more obscure functionality, listed on
// https://en.cppreference.com/w/cpp/header/exception
}

They are useful primarily in concurrent programs for:

  • capturing a running exception, passing std::exception_ptr to a different thread and repropagating it there

  • creating std::future objects holding exceptions to signal a failure in satisfying std::promise (a mechanism of asynchronous communication)

An external article describes "Lippincott technique" which can be used as a pattern to implement the same uniform exception handling in multiple places.

Summary

  • write handlers in order of decreasing specificity

  • catch by const reference

  • when rethrowing the same exception object, write just throw;, otherwise the exception will be sliced