Unfortunately when exceptions occur it's all to easy to corrupt a persistent store and cause permanent data loss.
It is not generally a good idea to catch exceptions in a try - catch(...)
block because
that will hide a lack of proper exception handling under the carpet.
In fact one should only catch exceptions that you expect are reasonable to occur and that can be safely handled.
We certainly don't want to catch access violations, division by zero etc.
A CSpaceLock
defines an atomic unit of work for the persistent store - i.e. it is associated with a
transaction. A CSpaceLock
also tells the system to update the display as required. So the atomic
unit of work relates both to changes on disk as well as changes on the screen.
Consider the following example
void foo()
{
// Acquire a lock on the CSpace for the scope of this function
CSpaceLock lock;
MakeChangesToPersistentObjects();
}
It is all too easy for an exception to be thrown within MakeChangesToPersistentObjects()
, perhaps
part way through its execution and leaving persistent objects in an invalid state. It would be
very bad to propagate these changes to disk. Of course a correct design would not allow this to
happen in the first place, but it is not uncommon for a programmer to forget to account for
exceptions.
The usual behaviour for an RAII class used to lock the CSpace
would be to automatically and
silently commit the transaction when an exception is thrown and the stack unwinds.
std::uncaught_exceptions()
can be used to detect whether a CSpaceLock
is
destructing because an exception was thrown in the scope in which it was declared and the stack
is unwinding.
We can call std::terminate()
when uncaught exceptions are thrown inside
a CSpaceLock
.
class CSpaceLock
{
public:
~CSpaceLock()
{
if (uncaughtExceptionCount_ != std::uncaught_exceptions())
{
// Stack is unwinding, we assume it is too risky to allow
// a transaction to commit, and instead terminate the process
cxAlwaysAssert( false );
std::terminate();
}
}
private:
int uncaughtExceptionCount_ = std::uncaught_exceptions();
};
The calls to std::uncaught_exceptions()
impact the rate at which a CSpaceLock
can be used
to lock a CSpace
.
The rate drops from about 34 MHz to 14 MHz on a windows-64 build with AMD Ryzen 9 3900X 12-core machine.
[please ignore: this is the old approach and is no longer the case]
To force the programmer to be conscientious with exception programming, it is a requirement that a
CSpaceLock
be explicitly committed before it destructs. So the above code must instead be written as
follows
void foo()
{
CSpaceLock lock;
MakeChangesToPersistentObjects();
// Every CSpaceLock must be explicitly committed before it is allowed to destruct
lock.Commit();
}
Now if an exception is thrown by MakeChangesToPersistentObjects()
, the CSpaceLock
will be destroyed
(as the stack unwinds) without it being committed. This is regarded as a design error, and the
system disables further writing to the persistent store and aborts the process!
This forces the programmer to handle exceptions. Of course it is reasonable for exceptions to be thrown in a deeply nested function call, and the exception is caught and handled a long way from where it was thrown.