The Auto Macro: A Clean Approach to C++ Error Handling

The Auto Macro:  A Clean Approach to C++ Error Handling

Every professional C++ engineer sooner or later asks herself a question: “How do I write exception safe code?” The problem is far from trivial:

Consider the following example:

bool Mutate(Logger* logger)
{
logger->DisableLogging();
AttemptOperation();
AttemptDifferentOperation();
logger->EnableLogging();
return true;
}

If AttemptOperation() throws an exception we will not call EnableLogging() on the logger object. If you don’t use exceptions and use return codes instead to propagate errors the problem doesn’t go away. You use error codes, but you’ll realize that now you need to call EnableLogging() in multiple places.

bool Mutate(Logger* logger)
{
logger->DisableLogging();
if (!AttemptOperation())
{
logger->EnableLogging();
return false;
}
if (!AttemptDifferentOperation())
{
logger->EnableLogging();
return false;
}

logger->EnableLogging();
return true;
}

The remedy is well researched: Use a try-catch block or a so-called Auto object, whose sole purpose is to call EnableLogging() in its destructor. Auto object is better because it’s exception agnostic, it will work regardless of the fact if you use exceptions or not.

class AutoDisableLogging
{
public:
AutoDisableLogging(Logger* logger): m_logger(logger)
{
m_logger->DisableLogging();
}

~AutoDisableLogging()
{
m_logger->EnableLogging();
}

private:
Logger* m_logger;
};

(Class above should also have assignment operator and copy constructor defined as it has custom destructor, but I’ve omitted them for clarity).

Now you can safely put AutoDisableLogging autoDisableLogging; in the beginning of your function and be safe. However, the number of such operations quickly grows and it becomes impractical to create auto classes for all of such cases.

Ideally, you could separate Auto part from Enable/DisableLogging() part and write something like this:

bool Mutate(Logger* logger)
{
logger->DisableLogging();
Auto(logger->EnableLogging());
AttemptOperation();
AttemptDifferentOperation();
logger->EnableLogging();
return true;
}

which ensures that the statement passed in as a parameter is called at the end of the scope. If it could capture variables by reference you could write things like:


Transaction xact;
bool isCommited = false;
Auto(if(!isCommited) xact.Rollback());

if (!replicaNetworkThread.InitReplicateDatabase())
return false;
if (!InsertReplicaRecord(xact))
return false;
if (!dbMgr->PrepareReplicationThreads())
return false;

xact.Commit();
// No failures beyond this point
isCommited = true;

....

return true;

Notice that the value of isCommited is not saved at the time Auto() statement is seen the first time, but is read at the time when statement inside Auto is executed.

If you use Auto() multiple times in the same scope, order of execution of Auto’s at the end of the scope must be reversed:


dbMgr.StartChangingReplayState();
Auto(dbMgr.StopChangingReplayState());

dbMgr.AcquireTablesLock();
Auto(dbMgr.ReleaseTablesLock());

dbMgr.AcquireBeginSnapshotLock();
Auto(dbMgr.ReleaseBeginSnapshotLock());

How would you implement Auto() described above?

You can use a macro and construct C++11 lambda around the statement passed in to macro and do something like:

class AutoOutOfScope
{
public:
AutoOutOfScope(std::function<void()> destructor)
: m_destructor(destructor) { }
~AutoOutOfScope() { m_destructor(); }
private:
std::function<void()> m_destructor;
}

#define Auto(Destructor) AutoOutOfScope auto_oos([&]() { Destructor; })

There are couple of problems with this implementation. First problem is the duplicate name conflict on “auto_oos” if Auto() is used more than once in the same scope. We can solve this with concatenating special macro COUNTER at the end of our name (Why don’t we use LINE instead?).


#define TOKEN_PASTEx(x, y) x ## y
#define TOKEN_PASTE(x, y) TOKEN_PASTEx(x, y)

class AutoOutOfScope
{
public:
AutoOutOfScope(std::function<void()> destructor)
: m_destructor(destructor) { }
~AutoOutOfScope() { m_destructor(); }
private:
std::function<void()> m_destructor;
}

#define Auto(Destructor) AutoOutOfScope TOKEN_PASTE(auto_, __COUNTER__)([&]() { Destructor; })

Second problem is that conversion to std::function will allocate memory internally which we would like to avoid. We can avoid it by templatizing AutoOutOfScope class and passing lambda without converting it to std::function<void()>. With both of these issues fixed code will look like this:

template
class AutoOutOfScope
{
public:
AutoOutOfScope(T& destructor) : m_destructor(destructor) { }
~AutoOutOfScope() { m_destructor(); }
private:
T& m_destructor;
};

#define Auto_INTERNAL(Destructor, counter) \
auto TOKEN_PASTE(auto_func_, counter) = [&]() { Destructor; };
AutoOutOfScope<decltype(TOKEN_PASTE(auto_func_, counter))> TOKEN_PASTE(auto_, counter)(TOKEN_PASTE(auto_func_, counter));

#define Auto(Destructor) Auto_INTERNAL(Destructor, __COUNTER__)

That is it! There are close to 500 usages of Auto on the SingleStore codebase. It has become one of our most favourite abstractions.


Share