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 and catch 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 and thread_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.