In their original article on
ScopeGuard, Andrei Alexandrescu and Petru
Marginean demonstrate a very clever C++
technique to give programmers the safety and
benefits of custom RAII objects without requiring one
to create custom classes for each type of use.
ScopeGuard was created because, in their own
words:
Solutions that require a lot of discipline and grunt work are not very attractive.
Under schedule pressure, a good but clumsy solution loses its utility. Everybody
knows how things must be done by the book, but will consistently take the shortcut.
The one true way is to provide reusable solutions that are correct and easy to use.
While their solution is correct and easy
to use, it could be even easier. Their solution
requires two creator functions,
MakeGuard and
MakeObjGuard , and allows for the use
of two corresponding macros,
ON_BLOCK_EXIT and
ON_BLOCK_EXIT_OBJ . Further,
MakeObjGuard and
ON_BLOCK_EXIT_OBJ take the object
reference first followed by the method pointer.
Their solution deviates from
std::tr1::bind 's approach.
std::tr1::bind can deduce whether you
are passing in a pointer to a free-function or to
a method. Also, bind passes the
arguments in a different order, passing the method
pointer first and the object second. It may be
helpful to think of the object pointer as the
this pointer which is the first
parameter to a member function.
Our goal is to avoid having any negative impact
on existing ScopeGuard consumers, remaining
backward compatible, while making the following
code work correctly:
struct base { void func(); };
struct derived : base { };
base b;
derived d;
ON_BLOCK_EXIT(&base::func,b); //Line A
ScopeGuard guard = MakeGuard(&base::func,d); //Line B
My first approach was to overload the creator
function MakeGuard as follows,
repeated for each variable number of arguments.
For brevity, I show only the code for zero
parameters:
template <typename Ret, class Obj>
inline ObjScopeGuardImpl0<Obj,Ret(Obj::*)()> MakeGuard(Ret(Obj::*memFun)(), Obj &obj) {
return ObjScopeGuardImpl0<Obj,Ret(Obj::*)()>::MakeObjGuard(obj,memFun);
}
In my sample code above, Line A
will compile. The compiler selects the new
MakeGuard overload, and binds
Obj to be base . A
pointer to a method on base is
supplied as the first parameter, and
b , a reference to a
base , is supplied as the second
parameter.
However, Line B fails to compile.
Can you see why? The problem is due to C++'s
overload resolution rules combined with templates.
When selecting a proper overload, the compiler
will not cast any template parameters.
Line B fails to compile because
there is no choice for Obj which does
not require at least one of the function
parameters to be casted. If Obj were
bound to base , then the second
parameter, which is of type reference to
derived would need to be cast to a
reference to base . While this is a
legal cast, no casts are legal when templates are
involved in function overloads. If
Obj were bound to
derived , then the first parameter,
which is of type void(base::*)()
would need to be cast to type
void(derived::*)() . Again, while this
is a legal cast, no cast is permitted in this
situation.
The fix is to template the
MakeGuard overload on the object type
twice, once for the method's object type, and once
for the object type of the supplied parameter. If
the two are not compatible,
ObjScopeGuardImplN will fail to
compile the Execute() method. Here is
the correct code for zero arguments:
template <typename Ret, class Obj1, class Obj2>
inline ObjScopeGuardImpl0<Obj1,Ret(Obj2::*)()> MakeGuard(Ret(Obj2::*memFun)(), Obj1 &obj) {
return ObjScopeGuardImpl0<Obj1,Ret(Obj2::*)()>::MakeObjGuard(obj,memFun);
}
Line B now compiles correctly.
Obj1 matches derived ,
and Obj2 matches base .
It is legal to call a method on a base class
through a reference to a derived class, so
ObjScopeGuardImpl0::Execute()
compiles as well. Line A continues to
work correctly, as both Obj1 and
Obj2 match base .
Extending this out to multiple parameters is
relatively trivial, as long as you keep in mind
this same no-casting rule. The naive approach
would be to template MakeGuard on
P1 , then accept a pointer to a method
that takes a P1 as well as the
instance of a P1 to pass to that
method. However, this suffers the same problem
previously described. If the method took a float,
but the consumer supplied an integer, the overload
would not match and the code would fail to
compile.
Keeping this in mind, here are the two missing
methods. You will notice that the single argument
version is templated on P1a and
P1b . P1a is the type
that the object's method expects to receive, and
P1b is the type that the consumer
actually supplied. The same is true for the two
argument version.
//1-argument method
template <typename Ret, class Obj1, class Obj2, typename P1a, typename P1b>
inline ObjScopeGuardImpl1<Obj1,Ret(Obj2::*)(P1a),P1b> MakeGuard(Ret(Obj2::*memFun)(P1a), Obj1 &obj, P1b p1) {
return ObjScopeGuardImpl1<Obj1,Ret(Obj2::*)(P1a),P1b>::MakeObjGuard(obj,memFun,p1);
}
//2-argument method
template <typename Ret, class Obj1, class Obj2, typename P1a, typename P1b, typename P2a, typename P2b>
inline ObjScopeGuardImpl2<Obj1,Ret(Obj2::*)(P1a,P2a),P1b,P2b> MakeGuard(Ret(Obj2::*memFun)(P1a,P2a), Obj1 &obj, P1b p1, P2b p2) {
return ObjScopeGuardImpl2<Obj1,Ret(Obj2::*)(P1a,P2a),P1b,P2b>::MakeObjGuard(obj,memFun,p1,p2);
}
One final change is to supply three additional
overloads which allow consumers to supply object
pointers rather than object references. Again,
this is something that std::tr1::bind
does. These three overloads are trivial and I will
not show them here.
Andrei has approved of these changes and
considers them an improvement. Click
here to download the new and improved version of
ScopeGuard
Joshua Lehrer Email: c p p w e b at
lehrerfamily dot com FactSet Research
Systems Homepage
|