Generic<Programming>: Change the Way You Write Exception-Safe Code ...
source link: https://www.drdobbs.com/cpp/generic-change-the-way-you-write-excepti/184403758?pgno=2
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
C/C++
Generic: Change the Way You Write Exception-Safe Code — Forever
By Andrei Alexandrescu and Petru Marginean, December 01, 2000
Let's face it: Writing exception-safe code is hard. But it just got a lot easier with this amazing template.
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_;
};
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.*
.
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK