Calling All Members: Member Functions as Callbacks

Dale Rogerson
Microsoft Developer Network Technology Group

Created: April 30, 1992

Click to open or copy the files in the CALLB sample application for this technical article.

Abstract

Microsoft® Windows™ version 3.1 has over 30 callback functions that applications can use to enumerate objects, hook into the hardware, and perform a variety of other activities. Due to the prevalence of callbacks, it is only natural to want to handle callbacks with C++ member functions. However, callbacks are prototyped as C functions and, therefore, do not associate data with operations on that data, making the handling of callbacks less straightforward when you use C++ than it initially might appear.

This article explains why normal member functions cannot be used as callback functions, gives several techniques for handling callbacks, and illustrates these techniques with code fragments. The code fragments are included as the CALLB sample program on the Microsoft Developer Network CD.

The article and source code are targeted toward Microsoft C/C++ version 7.0, but the ideas presented apply to all C++ compilers, including those by Borland and Zortech.

The reader should be familiar with Windows callbacks and with C++. A bibliography is supplied at the end of the article.

The Hidden Parameter, the this Pointer

Every callback function has its own prototype, which determines the parameters that the Microsoft® Windows™ operating system passes to the function. For example, EnumObjects is a Windows function that enumerates objects inside of Windows, such as pens and brushes (these objects should not be confused with C++ objects). EnumObjectsProc is the callback for EnumObjects and is prototyped this way:

int FAR PASCAL __export EnumObjectsProc( LPSTR lpLogObject, LPSTR 
      lpData) ;

Note   CALLBACK can be used in place of FAR PASCAL above.

When Windows calls the EnumObjectsProc function, it passes the two parameters—lpLogObject and lpData—to the function.

The following code attempts to set up a member function as a callback. The code compiles and links successfully but causes a protection fault at run time.

// See CProg1.cpp
// Run nmake -fmake1
class CProg1 {
   private:
      int nCount ;
      // Incorrect callback declaration
      // Use a static or nonmember function.
         int FAR PASCAL EXPORT 
               EnumObjectsProc( LPSTR lpLogObject, LPSTR lpData) ;
   public:
         // Constructor
            CProg1() : nCount(0) {};
         // Member function
            void enumIt(CDC& dc) ;
};

void CProg1::enumIt(CDC& dc)
{
   // Register callback
      dc.EnumObjects(OBJ_BRUSH, EnumObjectsProc, NULL) ;
}

// Callback handler
int FAR PASCAL EXPORT 
CProg1::EnumObjectsProc( LPSTR lpLogObject, LPSTR pData)
{
   // Process the callback.
      nCount++ ;
      MessageBeep(0) ;
      return 1 ;
}

If the Windows ::EnumObjects function is called instead of CDC::EnumObjects, as in this line:

::EnumObjects(hdc, OBJ_BRUSH, (FARPROC)EnumObjectsProc, NULL) ;

the following error would occur:

cprog1.cpp(13) : error C2643: illegal cast from pointer to member

The reason for the above error and protection fault is that C++ member functions have a hidden parameter known as the this pointer. C++ is able to associate a function with a particular instance of an object by means of the this pointer. When C++ compiles the following line:

dc.EnumObjects(OBJ_BRUSH, EnumObjectsProc, NULL) ;

it generates a call equivalent to:

CDC::EnumObjects(OBJ_BRUSH, EnumObjectsProc, NULL, (CDC *)&dc) ;

The last parameter, (CDC*) &dc, is the this pointer. Member functions access an object's data through the this pointer. C++ handles the this pointer implicitly when accessing member data. In the CProg1::enumIt function, the line:

nCount = 0 ;

is actually compiled this way:

this->nCount = 0 ;

Windows passes only two parameters to EnumObjectsProc. It does not call functions through objects and cannot send a this pointer to the callback function. However, as compiled above, EnumObjectsProc expects three parameters instead of two. The result is that a random value on the stack is used as the this pointer, causing a crash. To handle EnumObjectsProc as a member function, the compiler must be told not to expect a this pointer as the last parameter.

Avoiding the this Pointer

Two function types in C++ do not have a this pointer:

Nonmember Functions

A nonmember function is not part of a C++ class and, therefore, does not have a this pointer. A nonmember function does not have access to the private or protected members of a class. However, a nonmember friend function can access the private and protected class members with which the function is friendly. Using nonmember functions to handle a callback is similar to handling a callback in C.

Static Member Functions

Static member functions are class member functions that do not receive this pointers. As a result:

The last point above is the kicker. Unlike a nonstatic member function, a static member function is not bound to an object. A static function cannot implicitly access nonstatic members.

For more information on static member functions, see the bibliography at the end of this article.

Techniques for Handling Callbacks

The rest of this article demonstrates techniques for handling callbacks with static member functions. The main concern is linking the callback routine with a particular object by providing a pointer to the object—kind of a pseudo-this pointer. In other words, our goal is to make a static function act like a nonstatic function. You can use the following techniques to achieve this goal:

The callback being handled will determine the technique to use. Many callbacks do not have a parameter for application-supplied data, nor do they return a unique value.

Not Providing a Pointer

In some cases, object pointers are unnecessary because the callback does not need to access member data. In these cases, the callback operates only on static data. The following code fragment demonstrates the technique.

// See CProg3.cpp
// Run nmake -fmake3
class CProg3 {
   private:
      static int statCount ;
      int nCount ;
      // Use a static member function for callbacks.
         static int FAR PASCAL EXPORT 
               EnumObjectsProc( LPSTR lpLogObject, LPSTR lpData) ;
   public:
      // Constructor
         CProg3() : nCount(0) {};
      // Member function
         void enumIt(CDC& dc) ;
};

// Static data members must be defined.
int CProg3::statCount = 0 ;

// Enumerate the Windows DC objects.
void CProg3::enumIt(CDC& dc)
{
   // Register callback and start enumerating.
      dc.EnumObjects(OBJ_BRUSH, EnumObjectsProc, NULL) ;
}

// Callback handler
int FAR PASCAL EXPORT 
CProg3::EnumObjectsProc( LPSTR lpLogObject, LPSTR pData)
{
   // Process the callback.
      statCount++;
   // nCount++;  This line would cause an error if not commented.
      MessageBeep(0) ;
      return 1 ;
}

Note that all objects of the CProg3 class above will share the statCount variable. Whether this is good or bad depends on what the application is trying to accomplish. The following code fragment illustrates how the outcome might not be what is expected.

void someFunc(CDC& aDC, CDC& bDC, CDC& cDC)
{
   // Assume that aDC has a = 3 objects.
   // Assume that bDC has b = 4 objects.
   // Assume that cDC has c = 7 objects.
   // Create some objects.
      CProg3 aObject;
      CProg3 bObject;
      CProg3 cObject;
      aObject.enumIt(aDC) ; // statCount = a = 3
      aObject.enumIt(bDC) ; // statCount = a + b = 7
      aObject.enumIt(cDC) ; // statCount = a + b + c = 14 
}

There are several ways to avoid the sharing of data between instances of a class. The next sections describe techniques that link the callback function to a particular object by providing a pseudo-this pointer.

Providing a Pointer in a Static Member Variable

The main reason to have a callback as a member function is for accessing class members unique to a particular object (that is, nonstatic members). A callback member function must be a static function and, therefore, can only access static members without using "." or "->".

The next listing shows how to use a static member variable to pass an object's this pointer to the callback. The callback can then use the pointer to access object members. To simplify the code, the callback calls a helper function that performs all the work. The helper function is nonstatic and can implicitly access member data through its this pointer.

// See CProg5.cpp
// Run nmake -fmake1
class CProg5 {
   private:
      int nCount ;
      // Use a static variable to pass the this pointer.
         static CProg5 * pseudoThis ;
   
      // Use a static member function for callbacks.
         static int FAR PASCAL EXPORT 
               EnumObjectsProc( LPSTR lpLogObject, LPSTR lpData) ;
      
      // Use a nonstatic member function as a helper.
         int EnumObjectsHelper( LPSTR lpLogObject, LPSTR lpData);
   public:
      CProg5() : nCount(0) {};
      void enumIt(CDC& dc) ;
};

// Static data members must be defined.
CProg5 * CProg5::pseudoThis = NULL;

// Enumerate the objects.
void CProg5::enum(CDC& dc)
{
   pseudoThis = this ;
   // Register callback and start enumerating.
      dc.EnumObjects(OBJ_BRUSH, EnumObjectsProc, NULL) ;
      pseudoThis = NULL ;
}

// Callback handler
int FAR PASCAL EXPORT 
CProg5::EnumObjectsProc( LPSTR lpLogObject, LPSTR lpData)
{
   if (pseudoThis != (CProg *)NULL)
      return pseudoThis->EnumObjectsHelper(lpLogObject, lpData) ;
   else
      return 0 ;
}

int CProg5::EnumObjectsHelper( LPSTR lpLogObject, LPSTR lpData)
{
   // Process the callback.
      nCount++;
      MessageBeep(0) ;
      return 1 ;
}

While the above technique works fine in many cases, the objects must coordinate the use of the callback. For callbacks (such as EnumObjects) that do their work and then exit, coordination is not much of a problem. For other callbacks, it may be. The techniques described in the next two sections require less coordination but work only with certain callbacks.

Passing a Pointer in a Parameter for Application-Supplied Data

A close examination of the EnumObjects function reveals that it has an extra 32-bit parameter, lpData, for supplying data to the callback routine. This is a great place to pass a pointer to an object. The following overworked sample demonstrates this technique.

// See CProg6.cpp
// Run nmake -fmake1
class CProg6 {
   private:
      int nCount ;
      // Use a static member function for callbacks.
         static int FAR PASCAL EXPORT 
               EnumObjectsProc( LPSTR lpLogObject, LPSTR lpData) ;
      // Use a nonstatic member function as a helper.
         int EnumObjectsHelper( LPSTR lpLogObject) ;
   public:
      CProg6() : nCount(0) {};
      void enumIt(CDC& dc) ;
};

// Enumerate the objects.
void CProg6::enumIt(CDC& dc)
{
   // Register callback and start enumerating.
      dc.EnumObjects(OBJ_BRUSH, EnumObjectsProc, (LPSTR)this) ;
}

// Callback handler
int FAR PASCAL EXPORT 
CProg6::EnumObjectsProc( LPSTR lpLogObject, LPSTR lpData)
{
      CProg6 * pseudoThis = (CProg6 *)lpData ;
      if ( pseudoThis != (CProg6 *)NULL )
         return pseudoThis->EnumObjectsHelper(lpLogObject) ;
      else
         return 0 ;
}

// Callback helper function.
int CProg6::EnumObjectsHelper( LPSTR lpLogObject)
{
   // Process the callback.
      nCount++;
      MessageBeep(0) ;
      return 1;
}

This technique will, of course, only work with callbacks that take application-supplied data. The following list shows those callbacks:

Keeping a Pointer in a Collection Indexed by a Return Value

Another technique for linking an object pointer with a callback uses the return value of the function that sets up the callback. This return value is used as an index into a collection of object pointers.

In the following example, SetTimer sets up a TimerProc callback and returns a unique timer ID. The timer ID is passed to TimerProc each time the function is called. The CTimer class uses the timer ID to find the object pointer in a CMapWordToPtr collection. The CTimer class is an abstract class designed to be inherited by other classes.

// See CTimer.h
// Run nmake -ftmake

// Declaration
class CTimer
{
   private:
      UINT id ;
      static CMapWordToPtr timerList ;
      static void stopTimer(int id) ;
      static void FAR PASCAL EXPORT
         timerProc(HWND hwnd, UINT wMsg, int timerId, DWORD dwTime);

   protected:
      virtual void timer(DWORD dwTime) = 0 ;

   public:
      // Constructor
         CTimer() : id(NULL) {};
      // Destructor
         ~CTimer() {stop();};
      // Use
         BOOL start(UINT msec) ;
         void stop() ;
};

// Define statics.
CMapWordToPtr CTimer::timerList ;

// Implementation

BOOL CTimer::start (UINT msecs)
{
   id = SetTimer(NULL,0,msecs,(FARPROC)timerProc);
   if (id != NULL)
   {
      timerList.SetAt(id, this);
      return TRUE ;
   }
   else
      return FALSE;
}

void CTimer::stop()
{
      if (id != NULL)
      {
         stopTimer(id) ;
          id = NULL ;
      }
}


static void CTimer::stopTimer(int timerId) 
{
    KillTimer(NULL,timerId) ;
    timerList.RemoveKey(timerId) ;
}

static void FAR PASCAL EXPORT
CTimer::timerProc(HWND hwnd, UINT wMsg, int timerId, DWORD dwTime)
{
   CTimer * pseudoThis ;
   if ( timerList.Lookup(timerId, (void*&)pseudoThis))
   {
      if ( pseudoThis != (CTimer *)NULL)
           pseudoThis->timer(dwTime) ;
      else
         stopTimer(timerId) ;
   }
   else
      KillTimer(NULL,timerId) ;
 }

// Inherit CTimer class in order to use it.
class CMyTimer : public CTimer {
   protected:
      void timer(DWORD dwTimer) { MessageBeep(0); } ;
};

Conclusion

Static member functions are used in C++ to handle callbacks because they do not have this pointers. Callback functions are not designed to accept this pointers. Because static member functions do not have this pointers and, in many cases, it is desirable to have access to an object, this article has suggested four ways of providing the static member function with a this pointer.

Bibliography

For more information on C++ topics such as the this pointer, friend functions, or static functions, see:

For information on callbacks, see: