I l@ve RuBoard Previous Section Next Section

5.9 Handling Pointers to Member Functions

Although not very common in day-to-day programming activities, pointers to member functions can sometimes be of help. They act much like pointers to functions, but when you want to invoke them, you must pass an object (in addition to the arguments). The following example depicts the syntax and the semantics of pointers to member functions.



#include <iostream>


using namespace std;





class Parrot


{


public:


   void Eat()


   {


      cout << "Tsk, knick, tsk...\n";


   }


   void Speak()


   {


      cout << "Oh Captain, my Captain!\n";


   }


};





int main()


{


   // Define a type: pointer to a member function of


   // Parrot, taking no arguments and


   // returning void.


   typedef void (Parrot::* TpMemFun)();





   // Create an object of that type


   // and initialize it with the address of


   // Parrot::Eat.


   TpMemFun pActivity = &Parrot::eat;


   // Create a Parrot...


   Parrot geronimo;


   // ...and a pointer to it


   Parrot* pGeronimo = &geronimo;





   // Invoke the member function stored in Activity


   // via an object. Notice the use of operator.*


   (geronimo.*pActivity)();





   // Same, via pointer. Now we use operator->*


   (pGeronimo->*pActivity)();


   // Change the activity


   pActivity = &Parrot::Speak;





   // Wake up, Geronimo!


   (geronimo.*pActivity)();


}


A closer look at pointers to member functions and their two related operators—.* and ->*—reveals strange features. There is no C++ type for the result of geronimo.*p-Activity and pGeronimo->*pActivity. Both are binary operations all right, and they return something to which you can apply the function-call operator immediately, but that "something" does not have a type.[5] You cannot store the result of operator.* or operator->* in any way, although there is an entity that holds the fusion between your object and the pointer to a member function. This fusion seems to be very unstable. It appears from chaos just as you invoke operator.* or operator->*, exists just long enough to apply operator(), and then goes back to chaos. There's nothing else you can do with it.

[5] The standard says, "If the result of .* or ->* is a function, then that result can be used only as the operand for the function call operator ()."

In C++, where every object has a type, the result of operator->* or operator.* is a unique exception. It's even trickier than the ambiguity of pointers to functions introduced by overloading (discussed in the previous section). There we had too many types to choose from, but we could disambiguate the choice; here, we don't have any type to start with. For this reason, pointers to member functions and the two related operators are a curiously half-baked concept in C++. And by the way, you cannot have references to member functions (although you can have references to regular functions).

Some C++ compiler vendors define a new type and allow you to store the result of operator.* by using a syntax like this:



// __closure is a language extension


// defined by some vendors


void (__closure:: * geronimosWork)() =


   geronimo.*pActivity;


// Invoke whatever Geronimo is doomed to do


geronimosWork();


There's nothing about Parrot in the type of geronimosWork. This means that later, you can bind geronimosWork to something that's not geronimo, and even to something that's not a Parrot. All you have to commit to is the return type and the arguments of the pointer to a member function. In fact, this language extension is a kind of Functor class, but restricted to objects and member functions only (it does not allow regular functions or functors).

Let's implement support for bound pointers to member functions in Functor. The experience with functors and functions suggests that it's good to keep things generic and not to jump too early into specificity. In the implementation of MemFunHandler, the object type (Parrot in the previous example) is a template parameter. Furthermore, let's make the pointer to a member function a template parameter as well. By doing this, we get automatic conversions for free, as happened with the implementation of FunctorHandler.

Here's the implementation of MemFunHandler. It incarnates the ideas just discussed, plus the ones already exploited in FunctorHandler.



template <class ParentFunctor, typename PointerToObj,


   typename PointerToMemFn>


class MemFunHandler


   : public FunctorImpl


      <


         typename ParentFunctor::ResultType,


         typename ParentFunctor::ParmList


      >


{


public:


   typedef typename ParentFunctor::ResultType ResultType;


   MemFunHandler(const PointerToObj& pObj, PointerToMemFn pMemFn)


   : pObj_(pObj), pMemFn_(pMemFn) {}





   MemFunHandler* Clone() const


   { return new MemFunHandler(*this); }


   ResultType operator()()


   {


      return ((*pObj_).*pMemFn_)();


   }





   ResultType operator()(typename ParentFunctor::Parm1 p1)


   {


      return ((*pObj_).*pMemFn_)(p1);


   }





   ResultType operator()(typename ParentFunctor::Parm1 p1,


    typename ParentFunctor::Parm2 p2)


   {


      return ((*pObj_).*pMemFn_)(p1, p2);


   }





private:


   PointerToObj pObj_;


   PointerToMemFn pMemFn_;


};


Why is MemFunHandler parameterized with the type of the pointer (PointerToObj) and not with the type of the object itself? A more straightforward implementation would have looked like this:



template <class ParentFunctor, typename Obj,


   typename PointerToMemFn>


class MemFunHandler


   : public FunctorImpl


      <


         typename ParentFunctor::ResultType,


         typename ParentFunctor::ParmList


      >


{


private:


   Obj* pObj_;


   PointerToMemFn pMemFn_;


public:


   MemFunHandler(Obj* pObj, PointerToMemFn pMemFn)


   : pObj_(pObj), pMemFn_(pMemFn) {}


   ...


};


This code would have been easier to understand. However, the first implementation is more generic. The second implementation hardwires the pointer-to-object type in its implementation. It stores a bare, bald, unadorned pointer to Obj. Can this be a problem?

Yes it is, if you want to use smart pointers with MemFunHandler. Aha! The first implementation supports smart pointers; the second does not. The first implementation can store any type that acts as a pointer to an object; the second is hardwired to store and use only simple pointers. Moreover, the second version does not work for pointers to const. Such is the negative effect of hardwiring types.

Let's put in place a test run for the newly implemented feature. Parrot gets reused.



#include "Functor.h"


#include <iostream>


using namespace std;





class Parrot


{


public:


   void Eat()


   {


      cout << "Tsk, knick, tsk...\n";


   }


   void Speak()


   {


      cout << "Oh Captain, my Captain!\n";


   }


};





int main()


{


   Parrot geronimo;


   // Define two Functors


   Functor<>


      cmd1(&geronimo, &Parrot::Eat),


      cmd2(&geronimo, &Parrot::Speak);


   // Invoke each of them


   cmd1();


   cmd2();


}


Because MemFunHandler took the precaution of being as generic as possible, automatic conversions come for free—just as they did for FunctorHandler.

    I l@ve RuBoard Previous Section Next Section