Exceptions in C++
Storm is exception-aware. As such, it is generally safe to throw any C++ exception through Storm code. However, the following points need to be taken into account:
-
Storm assumes that all exceptions inherit from
core.Exception. - Special care is needed when throwing exceptions that contain pointers to GC-allocated objects.
As mentioned, the first point does not mean that it is not possible to try and catch other objects in C++ code linked to Storm. Any exception will "pass through" Storm code just fine. It only means that it is not possible to catch such exceptions from code generated by Storm (e.g. in Basic Storm), nor that storm code can throw such exceptions.
Defining exception classes
All exceptions that should be available to Storm code need to inherit from
core.Exception. Furthermore, they need to be defined slightly differently to play nice
with C++ and dynamic linking (in particular, C++ performs dynamic_cast internally which assumes
that there is only one instance of the C++ type info on Linux). This means that classes that inherit
from core.Exception needs to be defined differently in C++. Nothing special needs to be
done in Storm code, these extra steps are solely for the C++ interaction to work properly.
First, exceptions are marked with STORM_EXCEPTION(...) instead of STORM_CLASS. Secondly, the
exception also needs to have EXCEPTION_EXPORT before the class name. The former adds some extra
metadata members to the class that throw/catch needs to work properly. The latter makes sure that
the symbols are visible in dynamic libraries, which is needed for the C++ side of things to work
properly. This can look like below:
class EXCEPTION_EXPORT MyException : public storm::Exception {
STORM_EXCEPTION(storm::Exception);
public:
// ...
};
Note that the name of the superclass appears twice. Fortunately, Storm's preprocessor will verify that they are the same, and give you an error if you update one but forget to update the other.
Apart from these differences, an exception works exactly like a normal Storm-exposed class implemented in C++.
Throwing exceptions
As mentioned at the top of the page, it is not possible to throw exceptions that are GC-allocated without additional care. The reason is that some systems (e.g. Linux) stores the thrown exception in a location that is not visible to the GC. As such, the GC may incorrectly deem the exception object to be unreachable if it schedules a collection while an exception is in flight.
To solve the issue above, Storm requires that any types that are GC-allocated are wrapped in the C++
type Thrown<T> (where T is any type that inherits from core.Exception). That is, the
following code is incorrect:
MyException *e = new (...) MyException(); throw e; // Incorrect, will not behave properly!
Instead, we need to wrap the exception in Thrown<T> when throwing it:
MyException *e = new (...) MyException(); throw Thrown<MyException>(e);
Since this involves quite a bit of typing, Storm also provides raise(X) that wraps and throws the
exception correctly. As such, the following is the preferred way of throwing Storm exceptions.
raise(new (...) MyException());
The exception classes also contain a member raise that can be used to raise the
exception. However, due to the priority of the new operator one needs a set of parenthesis to call
it, which often makes the free function more convenient.
(new (...) MyException())->raise();
Again, note that only GC-allocated exceptions need to be wrapped in Thrown<T> or thrown using
raise(X). If you wish to throw other, C++-only exceptions, you can use throw as usual. However,
these will not be visible to Storm code as they do not inherit from core.Exception.
Catching exceptions
Since Storm exceptions are thrown wrapped inside Thrown<T>, they must also be catched as
Thrown<T>. As such, to catch a Storm exception in C++ you do as follows:
try {
raise(new (...) MyException());
} catch (const Thrown<MyException> &e) {
// ...
}
The Thrown<T> is designed to be a thin wrapper over a pointer. For example, it provides the ->
and * operators to make it easy to access members of the exception, as well as conversion
operators to the corresponding pointer type. This makes it possible to call message as follows:
try {
// ...
} catch (const Thrown<MyException> &e) {
Str *message = e->message();
PLN(message);
}
The underlying pointer can be extracted with the e.data() member if desired.
Note that Thrown<T> mirrors the class hierarchy of the exception types. This means that we can
still use inheritance to select which exceptions to catch. For example, the example belows works as
we expect. MyException inherits from Exception, and we expect the catch clause to be
applicable. This works because Thrown<MyException> inherits from Thrown<storm::Exception>.
try {
raise(new (...) MyException());
} catch (const Thrown<storm::Exception> &e) {
// ...
}
