05 - function-try-blocks
A hardly-ever used feature (as of writing this, I have never seen it in production code) are function-try-blocks. The feature lets embed entire function body in exception-handling code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
#include <cmath> #include <string> #include <iostream> int func(std::string number) try { // may throw std::invalid_argument and std::out_of_range int n = std::stoi(number); if (n < 0) throw std::runtime_error("value should be non-negative"); return std::sqrt(n); } catch (const std::runtime_error& e) { std::cout << "error: " << e.what() << "\n"; return 0; } catch (...) { return 0; } |
The feature is mostly just syntax sugar, however it is worth to mention that:
Function parameters live in both
try
andcatch
blocks. Parameters are constructed and destroyed before and after function call.Exceptions thrown by constructors (and destructors) of function parameters are not being caught. They are thrown at the call site, before (or after) the function is called.
For the main function and thread top-level functions, function-try-blocks do not catch exceptions thrown by constructors (and destructors) of
static
andthread_local
global objects.
In each catch
block you should do one of two things:
place a return statement (if the function returns non-void)
rethrow (
throw;
) or throw a different exception
Otherwise control reaches end of the function and since there is no return value neither an exception, return;
is assumed which is undefined behaviour for functions returning non-void.
Special member functions
A special use-case of this feature are constructors and destructors. The block begings before member initializer list which means derived classes can catch exceptions caused by base class constructors and member object constructors.
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 |
#include <stdexcept> class base { public: base(int x) { if (x < 0) throw std::runtime_error("x can not be negative"); } }; class derived : public base { public: derived(int x) try : base(x) { // ctor body } catch (const std::exception& /* e */) { // ... } catch (...) { // ... } }; |
Constructor failures cause the object to be thought as not constructed, therefore respective destructors are not being run.
Like in previous case, arguments have scope that cover both try
and catch
blocks, however non-static members (including members of parent classes) are destroyed upon entering any catch
block and thus are no longer safe to access (compiler might allow such code but it's undefined behaviour).
For constructors, return statements are not allowed in catch
blocks. Every catch
must terminate by throwing an exception. If no exception is thrown, rethrow (throw;
) is implicitly assumed.
For destructors, rethrow is also implicitly assumed but return;
statements are allowed.
How about delegating constructors?
From cppreference: If the function-try-block is on a delegating constructor, which called a non-delegating constructor that completed successfully, but then the body of the delegating constructor throws, the destructor of this object will be completed before any catch clauses of the function-try-block are entered.
Applications
The main purposes of function-try-blocks are:
(for constructors) responding to an exception thrown from the member initializer list by logging and rethrowing
modifying the exception object and rethrowing
throwing a different exception instead
terminating the program
Recommendation
This feature is hardly-ever used. The most typical usage would be to catch member/base class initialization failures in the deried class and (one of):
modifying the exception object (e.g. adding more context information) and rethrowing
throwing a different exception instead
In the most unpleasant scenario, the feature could be used to safely deal with throwing destructors of parent/member objects.