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: scop...@leh...ily.com
Homepage