Channels ▼
RSS

C/C++

Understanding C++ Function Try Blocks


C++ has some language constructs that are rarely seen in actual use. These constructs have their own valid use cases, but they should be used sparingly. As the now adage-status quote from The Old New Thing puts it: "Code is read much more often than it is written, so plan accordingly."

For unusual constructs, this advice holds true more than for any other code: If you use an unusual construct, make sure that the usage is justified and commented accordingly so the next person reading your code (which could even be you) will not need to spend too long to find out your original intentions.

In this article, I look at function try blocks, which have their own unique mechanics (hence, their own special gotchas), but are exactly the right solution for certain, occasional problems.

Use Case

The intended use case for function try blocks is to catch exceptions in constructor initialization lists, in base class constructors, and in destructors.

Function try blocks are the only way to handle exceptions thrown in member initializer lists in the context of the constructor. This is probably the most important use case for this construct, and we will take a look at an example where this is relevant. Function try blocks are also syntactically valid on regular functions, but of little use.

Syntax

The most important form of function try blocks is for constructors with initializer lists:

struct A : public B
{
    A() try : B(), foo(1), bar(2)
    {
        // constructor body 
    }
    catch (...)
    {
        // exceptions from the initializer list are caught here
        // but also rethrown after this block (unless the program is aborted)
    }

private:
    Foo foo;
    Bar bar;
};

Any exceptions that match the exception specification of the catch block(s) will be caught during the execution of the constructor (that is, not further up on the call stack).

For destructors (and it is the same for normal functions):

struct A
{
    ~A() 
    try
    {
        // destructor body
    }
    catch (...)
    {
        // exceptions from the destructor are caught here
        // but also rethrown after this block (unless the program is aborted)
    }
};

An important property of function try blocks is mentioned in the code comments: Any exceptions caught will be rethrown implicitly unless the program is aborted during the catch block.

When Is the Use of Function Try Blocks Justified?

Even though the most relevant information can be gathered in the context where the exception is thrown, this fact in itself probably would not justify the usage of this construct. Catching an exception in the constructor vs. somewhere further up the call stack is not substantially different, especially considering that the exception will be rethrown.

For thread-safe and performant copy construction, using a function try block is the best way to go (and most likely the motivating use case for the existence of this language feature). Consider the following example:

struct ExpensiveFoo
{
    ExpensiveFoo();
    ExpensiveFoo(ExpensiveFoo const& other);
    char data[10000];
};

struct Bar
{
    Bar() {}
    Bar(Bar const&);
	
private:
    ExpensiveFoo expensive;
    mutable std::mutex m;
};

Now, for purposes of example, let us make the following assumptions:

  1. The constructor of ExpensiveFoo takes a long time to execute (say, it uses a Web service to fill the data array with true random numbers from cosmic noise)
  2. The copy constructor of ExpensiveFoo is very cheap since it only copies the data, but it might throw an exception.

How can we implement the copy constructor of Bar? Note that we need a thread-safe implementation. A possible (naive) approach would be to use std::lock_guard:

Bar::Bar(Bar const& other)
{
	std::lock_guard(other.m);
	expensive = other.expensive;
}

This is not a bad implementation per se (it is thread safe and exception safe), but it comes at a huge cost. Since Bar::expensive is not initialized in the initializer list of the copy constructor, it will already be default-constructed when control reaches the beginning of the copy constructor. The default construction of this object, according to our assumption, is extremely expensive. So, how can we avoid this cost while still maintaining thread safety and exception safety? This is where the function try block is really useful.

Bar::Bar(Bar const& other) 
try : expensive((other.m.lock(), other.expensive))
{
    other.m.unlock();
}
catch(...)
{
    other.m.unlock();
}

You might have noticed that the initializer value of Bar::expensive looks unusual. We are taking advantage of the comma operator here. It will evaluate each subexpression from left to right and make the value of the right-most subexpression (other.expensive) the value of the whole expression, which then initializes Bar::expensive. This way, we can lock the mutex during the evaluation of this initializer value.

When the control flow reaches the beginning of the function try block, the mutex is in locked state and Bar::expensive is cheaply initialized (see the above assumptions: Copying ExpensiveFoo is cheap). No unnecessary ExpensiveFoo objects were created and destroyed. In the event that there are no exceptions during the construction, the try block will simply unlock the mutex.

If during the initialization of Bar::expensive an exception is thrown, the mutex would also be unlocked because the catch block would be executed. For reference, you can look at a compilable example of the source.

Without a function try block, both of these two requirements (performance and exception safety) could not be met easily. At least, not without refactoring the initialization of ExpensiveFoo into a separate init() function, thus sacrificing solid RAII semantics. However widespread such implementations are, they come at a cost. There is no guarantee that the object is fully initialized when it is in use; in other words, the class relies on the client code to perform the initialization. Either the implementation of the class has to be littered with checking a zombie flag during each operation (so it is able to meaningfully signal errors), or the client code is obliged to check the same flag. Neither way is idiomatic, modern C++. Function try blocks make it possible to remain in "RAII-land," making this a very justified use of an unusual construct.

Function Try Blocks for Regular Functions

For the sake of completeness, I should mention that function try blocks are occasionally seen with regular functions:

void function_with_try_block() 
try 
{
    // try block body
} 
catch (...) 
{ 
    // catch block body
}

is equivalent to:

void function_without_try_block() 
{ 
    try 
    { 
        // try block body
    } 
    catch (...) 
    { 
        // catch block body
    } 
}

However, they are of little use. The only advantage might be saving one level of indentation, but at the cost of using very unusual syntax. With the display resolutions today, that is not a good trade-off, so using function try blocks on regular functions is hardly justified.

Pitfalls

Function try blocks do come with surprising behavior.

  • Any exceptions caught in constructors or destructors are rethrown implicitly: "The currently handled exception is rethrown if control reaches the end of a handler of the function-try-block of a constructor or destructor" (15.3.15 C++ International Standard (Draft)/n3337). Hence, the most you can do when catching an exception in such context is to log it and possibly run some clean-up code. This is a good thing. If the construction of an object fails for whatever reason, you should not attempt to "save" that instance.
  • A return statement in the catch block of a function acts as if it were a return statement in the function.
  • The function returns when the control flow reaches the end of the catch block. If there is no return statement there and the function return type is non-void, the behavior is undefined.
  • Be aware that the function try block of main() has some non-intuitive behavior: Exceptions from constructors of objects defined on the namespace scope are not caught, and exceptions from destructors of objects with static duration are not caught.

Function try blocks should at least raise an eyebrow during code-review, especially if they are used for anything but the previously discussed use cases. On the other hand, they serve those needs well, so careful usage is indeed recommended.


Tamás Szelei works as a software developer in Budapest, Hungary, and frequently writes about the intricacies of C++.


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.
 

Video