Calling All Members: Member Functions as Callbacks

Dale Rogerson
Microsoft Developer Network Technology Group

Created: April 30, 1992

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 and SMART sample programs on this disc.

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.1 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

Static member functions

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:

An object does not have to be created before a static member function is called or static member data is accessed.

The class scope operator can access static members without an object, for example:

CFoo::someFunc(someValue)

A static member function cannot access a nonstatic member of its class without an object instance. In other words, all object access must be explicit, such as:

object.nonStatFunc(someValue);

// NOT: nonStatFunc(someValue) ;

or an object pointer, such as:

ptrObject->nonStatFunc(someValue);

// NOT: nonStatFunc(someValue) ;

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:

Not providing a pointer

Providing a pointer in a static member variable

Passing a pointer in a parameter for application-supplied data

Keeping a pointer in a collection indexed by a return value

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:

EnumChildProc

EnumChildWindows

EnumFontFamProc

EnumFontFamilies

EnumFontsProc

EnumMetaFileProc

EnumObjectsProc

EnumPropFixedProc

EnumPropMovableProc

EnumTaskWndProc

EnumWindowsProc

LineDDAProc

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:

Stroustrup, Bjarne. The C++ Programming Language. 2d ed. Addison-Wesley, 1991.

Ellis and Stroustrup. The Annotated C++ Reference Manual. Addison-Wesley, 1990.

Lippman, Stanley B. C++ Primer. 2d ed. Addison-Wesley, 1991.

Microsoft C/C++ version 7.0 C++ Language Reference. Microsoft Corporation, 1991.

Microsoft C/C++ version 7.0 C++ Class Libraries User's Guide, Microsoft Corporation, 1991.

For information on callbacks, see:

Microsoft Windows version 3.1 Software Development Kit (SDK) Programmer's Reference, Volume 1: Overview. Microsoft Corporation, 1987-1992.

Microsoft Windows version 3.1 SDK Programmer's Reference, Volume 2: Functions. Microsoft Corporation, 1987-1992.

Microsoft Windows version 3.1 SDK Guide to Programming. Microsoft Corporation, 1987-1992.

Petzold, Charles. Programming Windows. 2d ed. Microsoft Press, 1990.

Norton, Peter and Paul Yao. Peter Norton's Windows 3.0 Power Programming Techniques. Bantam Computer Books, 1990.

1These objects should not be confused with C++ objects.