Solution 4: Petru's Approach
Using the ScopeGuard
tool (which we'll explain in a minute), you can easily write code that's simple, correct, and efficient:
void User::AddFriend(User& newFriend) { friends_.push_back(&newFriend); ScopeGuard guard = MakeObjGuard( friends_, &UserCont::pop_back); pDB_->AddFriend(GetName(), newFriend.GetName()); guard.Dismiss(); }
guard
's only job is to call friends_.pop_back
when it exits its scope. That is, unless you Dismiss
it. If you do that, guard
no longer does anything. ScopeGuard
implements automatic calls to functions or member functions in its destructor. It can be helpful when you want to implement automatic undoing of atomic operations in the presence of exceptions.
You use ScopeGuard
like so: if you need to do several operations in an "all-or-none" fashion, you put a ScopeGuard
after each operation. The execution of that ScopeGuard
nullifies the effect of the operation above it:
friends_.push_back(&newFriend); ScopeGuard guard = MakeObjGuard( friends_, &UserCont::pop_back);
ScopeGuard
works with regular functions, too:
void* buffer = std::malloc(1024); ScopeGuard freeIt = MakeGuard(std::free, buffer); FILE* topSecret = std::fopen("cia.txt"); ScopeGuard closeIt = MakeGuard(std::fclose, topSecret);
If all atomic operations succeed, you Dismiss
all guards. Otherwise, each constructed ScopeGuard
will diligently call the function with which you initialized it.
With ScopeGuard
you can easily arrange to undo various operations without having to write special classes for removing the last element of a vector, freeing some memory, and closing a file. This makes ScopeGuard
a very useful reusable solution for writing exception-safe code, easily.
Implementing ScopeGuard
ScopeGuard
is a generalization of a typical implementation of the "initialization is resource acquisition" C++ idiom. The difference is that ScopeGuard
focuses only on the cleanup part you do the resource acquisition, and ScopeGuard
takes care of relinquishing the resource. (In fact, cleaning up is arguably the most important part of the idiom.)
There are different ways of cleaning up resources, such as calling a function, calling a functor, and calling a member function of an object. Each of these can require zero, one, or more arguments.
Naturally, we model these variations by building a class hierarchy. The destructors of the objects in the hierarchies do the actual work. The base of the hierarchy is the ScopeGuardImplBase
class, shown below:
class ScopeGuardImplBase { public: void Dismiss() const throw() { dismissed_ = true; } protected: ScopeGuardImplBase() : dismissed_(false) {} ScopeGuardImplBase(const ScopeGuardImplBase& other) : dismissed_(other.dismissed_) { other.Dismiss(); } ~ScopeGuardImplBase() {} // nonvirtual (see below why) mutable bool dismissed_; private: // Disable assignment ScopeGuardImplBase& operator=( const ScopeGuardImplBase&); };
ScopeGuardImplBase
manages the dismissed_
flag, which controls whether derived classes perform cleanup or not. If dismissed_
is true
, then derived classes will not do anything during their destruction.
This brings us to the missing virtual
in the definition of ScopeGuardImplBase
's destructor. What polymorphic behavior of the destructor would you expect if it's not virtual? Hold your curiosity for a second; we have an ace up our sleeves that allows us to obtain polymorphic behavior without the overhead of virtual
functions
For now, let's see how to implement an object that calls a function or functor taking one argument in its destructor. However, if you call Dismiss
, the function/functor is no longer invoked.
template <typename Fun, typename Parm> class ScopeGuardImpl1 : public ScopeGuardImplBase { public: ScopeGuardImpl1(const Fun& fun, const Parm& parm) : fun_(fun), parm_(parm) {} ~ScopeGuardImpl1() { if (!dismissed_) fun_(parm_); } private: Fun fun_; const Parm parm_; };To make it easy to use
ScopeGuardImpl1
, let's write a helper function.
template <typename Fun, typename Parm> ScopeGuardImpl1<Fun, Parm> MakeGuard(const Fun& fun, const Parm& parm) { return ScopeGuardImpl1<Fun, Parm>(fun, parm); }
MakeGuard
relies on the compiler's ability to deduce template arguments for template functions. This way you don't need to specify the template arguments to ScopeGuardImpl1
actually, you don't need to explicitly create ScopeGuardImpl1
objects. This trick is used by standard library functions, such as make_pair
and bind1st
.
Still curious about how to achieve polymorphic behavior of the destructor without a virtual
destructor? It's time to write the definition of ScopeGuard
, which, surprisingly, is a mere typedef
:
typedef const ScopeGuardImplBase& ScopeGuard;
Now we'll disclose the whole mechanism. According to the C++ Standard, a reference initialized with a temporary value makes that temporary value live for the lifetime of the reference itself.
Let's explain this with an example. If you write:
FILE* topSecret = std::fopen("cia.txt"); ScopeGuard closeIt = MakeGuard(std::fclose, topSecret);
then MakeGuard
creates a temporary variable of type (deep breath here):
ScopeGuardImpl1<int (&)(FILE*), FILE*>
This is because the type of std::fclose
is a function taking a FILE*
and returning an int
. The temporary variable of the type above is assigned to the const
reference closeIt
. As stated in the language rule above, the temporary variable lives as long as the reference and when it is destroyed, the correct destructor is called. In turn, the destructor closes the file. ScopeGuardImpl1
supports functions (or functors) taking one parameter. It is very simple to build classes that accept zero, two, or more parameters (ScopeGuardImpl0
, ScopeGuardImpl2
...). Once you have these, you overload MakeGuard
to achieve a nice, unified syntax:
template <typename Fun> ScopeGuardImpl0<Fun> MakeGuard(const Fun& fun) { return ScopeGuardImpl0<Fun >(fun); } ...
We already have a powerful means of expressing automatic calls to functions. MakeGuard
is an excellent tool especially when it comes to interfacing with C APIs without having to write lots of wrapper classes.
What's even better is the preservation of efficiency, as there's no virtual call involved.
ScopeGuard for Objects and Member Functions
So far, so good, but what about invoking member functions for objects? It's not hard at all. Let's implement ObjScopeGuardImpl0
, a class template that can invoke a parameterless member function for an object.
template <class Obj, typename MemFun> class ObjScopeGuardImpl0 : public ScopeGuardImplBase { public: ObjScopeGuardImpl0(Obj& obj, MemFun memFun) : obj_(obj), memFun_(memFun) {} ~ObjScopeGuardImpl0() { if (!dismissed_) (obj_.*fun_)(); } private: Obj& obj_; MemFun memFun_; };
ObjScopeGuardImpl0
is a bit more exotic because it uses the lesser-known pointers to member functions and operator.*
. To understand how it works, let's take a look at MakeObjGuard
's implementation. (We availed ourselves of MakeObjGuard
in the opening section.)
template <class Obj, typename MemFun> ObjScopeGuardImpl0<Obj, MemFun, Parm> MakeObjGuard(Obj& obj, Fun fun) { return ObjScopeGuardImpl0<Obj, MemFun>(obj, fun); }
Now if you call:
ScopeGuard guard = MakeObjGuard( friends_, &UserCont::pop_back);
then an object of the following type is created:
ObjScopeGuardImpl0<UserCont, void (UserCont::*)()>
Fortunately, MakeObjGuard
saves you from having to write types that look like uninspired emoticons. The mechanism is the same when guard
leaves its scope, the destructor of the temporary object is called. The destructor invokes the member function via a pointer to a member. To achieve that, we use operator.*
.