Daniel Schemmel
In the previous installmentdecltype
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://daniel.schemmel.net/post/2015/a-tour-of-rare-c++-features-part-1
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;
}
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:
::std::bad_alloc
::operator new
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::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);
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
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
This post is part 2 of a series on rarely used C++ features. Continue to the next part, Part 3
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. ↩
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. ↩
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. ↩
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
. ↩