Error Handling
If you have read Herb Sutter's work on exceptions [2], you know that it is essential that destructors must not throw an exception. A throwing destructor makes it impossible to write correct code and can shut down your application without any warning.
The destructors of ScopeGuardImplX
and ObjScopeGuardImplX
call an unknown function or member function respectively. These functions might throw. In theory, you should never pass functions that throw to MakeGuard
or MakeObjGuard
. In practice (as you can see in the downloadable code), the destructor is shielded from any exceptions:
template <class Obj, typename MemFun> class ObjScopeGuardImpl0 : public ScopeGuardImplBase { ... public: ~ScopeGuardImpl1() { if (!dismissed_) try { (obj_.*fun_)(); } catch(...) {} } }
The catch(...)
block does nothing. This is not a hack. In the realm of exceptions, it is fundamental that you can do nothing if your "undo/recover" action fails. You attempt an undo operation, and you move on regardless whether the undo operation succeeds or not.
A possible sequence of actions in our instant messaging example is: you insert a friend in the database, you try to insert it in the friends_
vector and fail, and consequently you try to delete the user from the database. There is a small chance that somehow the deletion from the database fails, too, which leads to a very unpleasant state of affairs.
In general, you should put guards on operations that you are the most sure you can undo successfully.
Supporting Parameters by Reference
We were happily using ScopeGuard
for a while, until we stumbled upon a problem. Consider the code below:
void Decrement(int& x) { --x; } void UseResource(int refCount) { ++refCount; ScopeGuard guard = MakeGuard(Decrement, refCount); ... }
The guard
object above ensures that the value of refCount
is preserved upon exiting UseResource
. (This idiom is useful in some resource sharing cases.)
In spite of its usefulness, the code above does not work. The problem is, ScopeGuard
stores a copy of refCount
(see the definition of ScopeGuardImpl1
, member variable parm_
) and not a reference to it. In this case, we need to store a reference to refCount
so that Decrement
can operate on it.
One solution would be to implement additional classes, such as ScopeGuardImplRef
and MakeGuardRef
. This is a lot of duplication, and it gets nasty as you implement classes for multiple parameters.
The solution we settled on consists of a little helper class that transforms a reference into a value:
template <class T> class RefHolder { T& ref_; public: RefHolder(T& ref) : ref_(ref) {} operator T& () const { return ref_; } }; template <class T> inline RefHolder<T> ByRef(T& t) { return RefHolder<T>(t); }
RefHolder
and its companion helper function ByRef
are ingenious; they seamlessly adapt a reference to a value and allow ScopeGuardImpl1
to work with references without any modification. All you have to do is to wrap your references in calls to ByRef
, like so:
void Decrement(int& x) { --x; } void UseResource(int refCount) { ++refCount; ScopeGuard guard = MakeGuard(Decrement, ByRef(refCount)); ... }
We find this solution to be pretty expressive and suggestive.
The nicest part of reference support is the const
modifier used in ScopeGuardImpl1
. Here's the relevant excerpt:
template <typename Fun, typename Parm> class ScopeGuardImpl1 : public ScopeGuardImplBase { ... private: Fun fun_; const Parm parm_; };
This little const
is very important. It prevents code that uses non-const
references from compiling and running incorrectly. In other words, if you forget to use ByRef
with a function, the compiler will not allow incorrect code to compile.
But Wait, There's More
You now have everything you need to write exception-safe code without having to agonize over it. Sometimes, however, you want the ScopeGuard
to always execute when you exit the block. In this case, creating a dummy variable of type ScopeGuard
is awkward you only need a temporary, you don't need a named temporary.
The macro ON_BLOCK_EXIT
does exactly that and lets you write expressive code like below:
{ FILE* topSecret = fopen("cia.txt"); ON_BLOCK_EXIT(std::fclose, topSecret); ... use topSecret ... } // topSecret automagically closed
ON_BLOCK_EXIT
says: "I want this action to be performed when the current block exists." Similarly, ON_BLOCK_EXIT_OBJ
implements the same feature for a member function call.
These macros use non-orthodox (albeit legal) macro wizardry, which shall go undisclosed. The curious can look them up in the code.
ScopeGuard in the Real World
Maybe the coolest thing about ScopeGuard
is its ease of use and conceptual simplicity. This article has detailed the entire implementation, but explaining ScopeGuard
's usage only takes a couple of minutes. Amongst our colleagues, ScopeGuard
has spread like wildfire. Everybody takes ScopeGuard
for granted as a valuable tool that helps in various situations, from premature returns to exceptions. With ScopeGuard
, you can finally write exception-safe code with reasonable ease and understand and maintain it just as easily.
Every tool comes with a use recommendation, and ScopeGuard
is no exception. You should use ScopeGuard
as it was intended as an automatic variable in functions. You should not hold ScopeGuard
objects as member variables, try to put them in vectors, or allocate them on the heap. For these purposes, the downloadable code contains a Janitor
class, which does exactly what ScopeGuard
does, but in a more general way at the expense of some efficiency.
Conclusion
We have presented the issues that arise in writing exception-safe code. After discussing a couple of ways of achieving exception safety, we have introduced a generic solution. ScopeGuard
uses several generic programming techniques to let you prescribe function and member function calls when a ScopeGuard
variable exits a scope. Optionally, you can dismiss the ScopeGuard
object. ScopeGuard
is useful when you need to perform automatic cleanup of resources. This idiom is important when you want to assemble an operation out of several atomic operations, each of which could fail.
Acknowledgements
The authors would like to thank to Mihai Antonescu for reviewing this paper and for making useful corrections and suggestions.
Bibliography
[1] Bjarne Stroustrup. The C++ Programming Language, 3rd Edition (Addison Wesley, 1997), page 366.
[2] Herb Sutter. Exceptional C++: 47 Engineering Puzzles, Programming Problems, and Solutions (Addison-Wesley, 2000).
Andrei Alexandrescu is a development manager at RealNetworks Inc. (www.realnetworks.com), based in Seattle, WA. He may be contacted at www.moderncppdesign.com.
Petru Marginean is senior C++ developer for Plural, New York. He can be reached at [email protected].