In this article I want to look at some applications for one of C++’s more obscure mechanisms, the function try-block.

Basic syntax

You’re (hopefully) familiar with the syntax of exceptions:

void func() { try { ... if (some_error) throw Exception { }; ... } catch (Exception& ex) { // Process exception... } }

We can have multiple catch blocks:

class Exception { }; class SpecificException : public Exception { }; void func() { try { ... if (some_error) throw SpecificException { }; ... } catch (SpecificException& ex) { // Process exception... } catch (Exception& ex) { // Process exception... } }

The thing to notice is that a try-block is just a scope. C++ extends this idea to functions; which of course are effectively just named blocks of code. The syntax is an extension of what we’ve just seen, but it does look a little weird (OK, a lot weird) if you’re not used to it.

void func() try { // Function body... if (some_error) throw Exception { }; ... } catch (SpecificException& ex) { // Process exception... } catch (Exception& ex) { // Process exception... }

Note the try and catch handlers are all outside the actual function body.

Any exceptions thrown inside the body of the function will be compared against the catch handlers at the end of the function.

This syntax doesn’t affect the function’s exception specification – as long as you catch all possible exceptions.

void cannot_throw() noexcept try { ... might_throw(); ... might_also_throw(); ... } catch(...) { // Handle any exception... } int main() { cannot_throw(); // Won't call terminate() }

As with any other catch block you can re-throw the exception:

void func() try { ... if (some_error) throw SpecificException { }; ... } catch (SpecificException& ex) { // Partial clean-up... throw; } int main() { try { func(); } catch (Exception& ex) { // Deal with exception... } }

What do function try-blocks give us that we can’t already achieve? Let’s explore a couple of applications.

Maintaining a single-point-of-exit

Building high-quality software means applying the Single Responsibility Principle. This is generally taken to be an object concept: an object should only have one reason to change. Or, put another way: an object should do one thing, completely and well. There is no reason not to also apply this to individual functions.

Let’s start with some C-style code to explore the idea (It’s easy to scoff but there is a lot of code like this in the embedded world)

In any function many things could go wrong, that cannot be dealt within the function itself. If the function cannot achieve its – single – purpose there is little left to do but clean-up and return (hopefully with some sort of error information, if you’re feeling generous)

ErrorType func() { ErrorType error { }; error = do_part_1(); if (error != OK) { // Perform clean up... return error; } error = do_part_2(); if (error != OK) { // Perform that same clean-up... return error; } error = do_part_3(); if (error != OK) { // Here's the clean-up code again... return error; } ... }

There’s a lot of repetition of clean-up code in this example.

Many programming standards impose the doctrine that there shall only be a single point-of-exit from a function. One way to achieve this is something like this:

ErrorType func() { ErrorType error { }; error = do_part_1(); if (error == OK) { ... error = do_part_2(); if (error == OK) { ... error = do_part_3(); if (error == OK) { ... } } } else { // Perform clean-up... } // Single point-of-exit // return error; }

We’ve achieved a single-point-of-exit but I’m not sure we’ve improved the readability, maintainability or testability of our code.

In C, one well-known idiom is the carefully controlled use of goto to ‘improve’ this code (some may argue that goto never improves code!)

ErrorType func() { ErrorType error { }; error = do_part_1(); if (error != OK) goto fail; error = do_part_2(); if (error != OK) goto fail; error = do_part_3(); if (error != OK) goto fail; goto exit; fail: // Clean-up code... exit: return error; }

This version has the advantage of providing a single-point-of-exit and removing all the nested if-statements. Additionally, it captures all the clean-up code in a single place and isolates it from the main algorithmic code.

Of course, probably the biggest problem with C-style exception handling is the propagation of errors to the point where they can be most effectively handled. C++ exceptions give us this mechanism.

Given that the code example above either completes its task or terminates, we could use a function try-block to capture this.

void func() try { ErrorType error { }; error = do_part_1(); if (error != OK) throw ExceptionA { }; error = do_part_2(); if (error != OK) throw ExceptionB { }; error = do_part_3(); if (error != OK) throw ExceptionC { }; ... } catch (Exception& ex) { // Clean-up code... throw; }

As with our previous example, the algorithmic code is separated from the clean-up code. We now have the benefit of a language-specified error propagation mechanism (exceptions); although we do pay a cost for this – in memory, and potential run-time performance and determinism.

This is the model that Ada adopted for its exception-handling mechanism. Ada exceptions handlers must be defined at the end of their functions.

Member Initialisation List exceptions

The primary purpose of function try-blocks is to capture exceptions in constructor Member Initialisation Lists (MILs).

The MIL is used – as the name suggests – to initialise class members using parameters passed into the constructor. Usually, the arguments are passed on un-modified but any (type-compatible) expression can be used as an initialiser – including functions.

Constructors

Let’s consider an example where a function is used as an initialiser; and that function could throw an exception:

double convert(int arg); // <= Might throw an exception class ADT { public: ADT(int init); private: double data { 0.0 }; }; ADT::ADT(int init) : data { convert(init) } // <= Exception happens here! { } int main() { ADT adt { 100 }; ... }

A function try-block gives us a mechanism to catch any exceptions thrown in the MIL:

ADT::ADT(int init) try : data { convert(init) } // Exception happens here! { } catch (std::exception& ex) { // Clean-up... }

Any fully constructed attributes or base classes will be fully destroyed before any catch clause is entered. Effectively, the object will cease to exist. Handlers for constructor function try-blocks therefore cannot access non-static members of the class.

There are a few other important subtleties to be aware of.

Exception re-throwing

First, the code above will terminate with an unhandled exception error! Unlike a ‘normal’ function try-block a MIL try-catch will automatically re-throw the caught exception.

The reasoning behind this is that, if an object cannot be initialised (via the MIL) then it cannot be valid state, so this must be reported to the client code. Incidentally, if an object’s MIL throws the object does not exist and cannot be used.

int main() { try { ADT adt { 100 }; ... } catch (std::exception& ex) { std::cout << ex.what() << std::endl; } // Carry on without adt, I guess... :-( }

We have to catch any thrown exceptions in the client code. There is no way of not re-throwing the exception.

Copy and move constructors

All constructors can use the function try-block notation, for example:

ADT::ADT(const ADT& src) try : data { src.data } { // Something that might throw // a std::exception... } catch (std::exception& ex) { // Handle it; then re-throw... }

Remember, if any of your constructors are marked noexcept – the move constructor is a prime example – a function try-block will not help, since it automatically re-throws its exception.

While we’re on the subject of copy constructors it’s worth noting that ‘normal’ function try-blocks won’t catch exceptions caused by objects passed by value (that is, thrown by the copy or move constructor). They must be caught by the client code.

void func(ADT adt) // <= Pass by value try { ... } catch (std::exception& ex) { // Any exception thrown by the ADT // copy constructor WON'T be caught // here. } int main() { try { ADT adt1 { 1 }; func(adt1); ... } catch (std::exception& ex) { // Exceptions from pass by-value // are caught here. } }

Destructors

You can throw (and catch) exceptions in destructors, but this is generally consider a fast-track to abnormal program termination. Perhaps we’ll explore this in another article on exception handling.

Finally, be careful with static objects. Static objects are constructed (by the run-time) and, of course, have the potential to throw exceptions from their constructors / MILs. The ability to catch those exceptions seems useful, too (almost vital, one might argue).

Placing a function try-block around main() seems to be a reasonable mechanism to achieve this.

ADT adt_global { 100 }; // Might throw int main() try { ... } catch (std::exception& ex) { // Handle static object exceptions // here?... }

Sadly, this doesn’t work; and there is no mechanism for catching exceptions thrown by static objects.

Summary

Function try-blocks are an obscure little corner of the C++ exception mechanism. Their syntax is unusual and their application is limited. They are certainly not an idiomatic part of ‘everyday’ C++ code. That said, it is worth understanding them and their limitations for that rare time you find them in code.

I’d love to know if you have a real-life use case for function try-blocks. Let me know in the comments below.