A Tour of Rare C++ Features 2 of 7: Function Try Blocks, Catch-All Exception Handlers and Rethrowing

In the previous installment we explored the combined use of trailing return types, decltype and multicharacter literals that together explain why the following snippet actually contains a correct main function:

/* This is valid C++ */  
auto main() -> decltype('O.o') try  
<%[O_O = 0b0]<%
https://gha.st/a-tour-of-rare-cpp-features/  
typedef struct o O;  
o*(*((&&o(o*o))<:'o':>))(o*o);  
if(O*O = decltype(0'0[o(0)](0))(0)) 1,000.00;  
else return 0==O==0 ? throw O_O : O_O;  
%>();%>
catch(...) { throw; }  

This time, we are going to explore how the body of this function can be correct. When declaring a function, its prototype (ending in this case in line 2 with decltype('O.o')) is terminated with a semicolon. On the other hand, when defining a function, the next token is usually an opening curly brace: {. So what is happening here?

As the title of this post already hints, this is a so-called function try block. Instead of starting a normal compound-statement, also known as block, which is the technical term for "a list of statements surrounded by curly braces", a function definition may instead be given by a try block followed by one or more exception handlers (catch blocks).

While there are some special rules with respect to scope and suppressing thrown exceptions in some cases, which will be explored later, a function try block mostly behaves just as a normal function which only contains the try/catch construct as its only statement.

That means the following two functions behave the same:

void f() {  
    try {
        throw 4;
    } catch(int) {
        return;
    }
}

void g() try {  
    throw 4;
} catch(int) {
    return;
}

Catch-All Exception Handlers

The last line of the example contains an exception handler with an ellipsis instead of an argument. This syntax is used if one wants to catch any and all exceptions. Obviously, one cannot refer to the actual exception anymore, as it has unknown type1.

Of course this makes it hard to deal with the exception, but four important options are still available:

  1. We can perform cleanup code that is independent of exactly how the code failed. After all, it is not important why we need to close a file we may have opened previously, just that it be closed correctly.
  2. We can suppress the exception. In some cases we know that an exception may be thrown without really caring about the type of the exception. This is very dangerous, as it will also suppress exceptions like ::std::bad_alloc, which is thrown when ::operator new fails.
  3. We can throw our own kind of exception instead (although it cannot wrap the inner exception, as it cannot access it). This allows us to write interfaces whose possible exceptions are known that still reuse code we may not really be sure about.
  4. We can perform some kind of cleanup, logging or other operation and then rethrow the exception. This is exactly what happens in the example and is discussed at length in the next part of this post.

Rethrowing Exceptions

In the example, there is a throw statement without anything to throw. This is how exceptions are correctly rethrown from inside an exception handler. Consider the following fragment2, where a file is opened, after which an operation is performed that may throw. Obviously we need to close the file either way.

FILE* a = ::std::fopen("cake.txt", "r");  
try {  
    eat(a);
} catch(::std::exception& e) {
    ::std::fclose(a);
    throw e;
}
::std::fclose(a);

Now, this code has three major problems: First, it assumes that eat will only ever throw exceptions that are derived from ::std::exception. Second, the stack trace that a debugger will show may very well show the throw in line 6 as the culprit that threw the exception. Finally, it changes the static type of the exception meaning it will now be considered a ::std::exception for the purpose of catching it, no matter what its type was when it was originally thrown.

All of these problems can be dealt with by rewriting the code to use a catch-all exception handler with a rethrow - we do not need to know the exception to rethrow it:

FILE* a = ::std::fopen("cake.txt", "r");  
try {  
    eat(a);
} catch(...) {
    ::std::fclose(a);
    throw;
}
::std::fclose(a);

What are function try blocks useful for?

So, if a function try block behave the same as if it would be wrapped in curly braces, why do they exist? Is this only a quirk of the C++ syntax requiring two characters less to type in a fairly rare case? Of course3 not, but their use is fairly restricted and intended for a very specific case.

Please consider the following piece of code:

struct A {  
    A() { throw "cake"; }
};

struct B : A {  
    B() : A() {
        try {
            // initialize B some more
        } catch(char const* reason) {
            ::std::cout << "Could not initialize B: " << reason << "\n";
            throw;
        }
    }
};

Here the constructor of B will also call the constructor of A. The idea is that an error during construction of B should be caught in the catch block and be logged to the console before being rethrown4.

However, the constructor of A is not performed inside the try block and its exception will therefore never be caught! This is shown by the explicit initialization of A in line 6, but would also happen if A where to be initialized implicitly instead. As you may have guessed, this problem can be solved by virtue of a function try block:

struct A {  
    A() { throw "cake"; }
};

struct B : A {  
    B() try : A() {
        // initialize B some more
    } catch(char const* reason) {
        ::std::cout << "Could not initialize B: " << reason << "\n";
        throw;
    }
};

Now, the initialization of A is performed inside the try block! If you compare the previous version to the new version using function try blocks, you will notice that both programs still crash (after all, we rethrow the exception), but the new version will perform the cleanup code in the exception handler.

Function try blocks have been around since the very first version of the C++ standard: ISO/IEC 14882:1998, although the wording has been changed with C++11.

Gimme more!

This post is part 2 of 7 of a series on rarely used C++ features. To find the other parts of the series around this example of how to not write C++, see this overview page.

Directly continue to the next part, Part 3.

Footnotes

  1. In C++, exceptions of arbitrary types may be thrown, i.e. unlike in Java they do not need to have some kind of common base class by which they could at least be used in some very generic way. Since exceptions may also be thrown by code in different translation units whose definition is not available, it is not even possible to define some kind of template exception handler, as the type of the exception may even be local to the other translation unit.

  2. In my opinion, this fragment should be rewritten in such a way that FILE pointers are not used raw, but rather wrapped inside a class that performs the proper cleanup in its destructor. In fact, most try/catch blocks should be rewritten in such a manner, as they are both unwieldy an usually hard to correctly reason about.

  3. Well, there are enough cases where it turns out that something really is only possible as an accident of the C++ syntax. But this time there really is a reason for it. I promise.

  4. Since the construction of the object failed, we cannot simply ignore the exception. It would however be reasonable to want to throw a different exception from inside the catch that is part of the interface of B instead of being part of the interface of A.

Daniel Schemmel

is currently employed at the Chair of Communication and Distributed Systems at RWTH Aachen University, where he researchs the testability of distributed systems. He can be reached at blog(at)gha.st.

Aachen, Germany, Terra, Sol, Milky Way, Laniakea SC https://gha.st/about/