How to provide a CALLBACK function into a C++ class object

AID: 655
  • Status: Published

9550 points

  • By
  • TypeTutorial
  • Posted on2009-05-16 at 05:39:03
Awards
  • Community Pick
  • Experts Exchange Approved

Some Windows API functions expect you to provide a pointer to a CALLBACK function that the system will need to call as part of the operation.  Such API functions as SetTimer, timeSetEvent, CreateThread, EnumWindows, LineDDA, even window message handler set by using RegisterClassEx or CreateDialog require such a function pointer.  Sorting functions, such as that used by the standard ListView control use a callback for comparing items.

But if you try to pass it the address of a C++ object member function, like so:

class MyTimer {
    ...
	void MyTimerProc();
    ...
}
int nTimerID = SetTimer( NULL, 1234, 1000, MyTimerProc );
                                  
1:
2:
3:
4:
5:
6:

Select allOpen in new window


...you will get a compiler error like:

   error C2643: illegal cast from pointer to member
   error C2664: 'SetTimer' : cannot convert parameter 4 from ... to ...

The reason has to do with the calling convention used for member functions.  In your program, when you call a member function, the compiler pushes the this pointer on the stack along with the other parameters.  But the API functions that will be using your callback expect to use the simpler calling convention.  

Let's start with a program that defines and uses a CBaseTimer object.   The following works because it uses a simple global function rather than a member function as the callback:

{{{{ Example 1 -- using a global (non-member) function }}}} 

#include <windows.h>
#include <stdio.h>
#include <Mmsystem.h>
#pragma comment(lib, "Winmm.lib" )

void CALLBACK MyTimerProc( UINT, UINT, DWORD, DWORD, DWORD ) {
    printf( "---------- timer tick ----------- \r\n");
}

class CBaseTimer {
public:
    CBaseTimer(int nIntervalMs) { m_nIntervalMs=nIntervalMs;}; // ctor
    ~CBaseTimer()                      { Kill(); };                   // dtor
    int m_nID;
    long m_nIntervalMs;

    void Start() {  // TBD: Check if 0 (failed)
        m_nID= timeSetEvent( m_nIntervalMs, 10, 
        MyTimerProc, 0, TIME_PERIODIC );
    }
    void Kill()  {
        ::timeKillEvent( m_nID );
    }
};
//------------------------------------- main() -- example usage of the object
int main(int argc, char* argv[])
{
    CBaseTimer cTimer( 1000 );
    cTimer.Start();
    for (int j=0; j< 40; j++) {
        Sleep(100);
        printf( "Hi there\r\n" );
    }
    return 0;
}
                                  
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:

Select allOpen in new window


But if you want the callback to be a member function:

class CBaseTimer {
...
    void CALLBACK MyTimerProc( UINT uID, UINT uMsg, DWORD dwUser, DWORD dw1, DWORD dw2 );
...
                                  
1:
2:
3:
4:

Select allOpen in new window


You will get the dreaded compiler error.  The common/documented way to avoid this is to make the callback into a static member function.

class CBaseTimer {
...
    static void CALLBACK MyTimerProc( UINT, UINT, DWORD, DWORD, DWORD  ); //<<--- note: "static"
...
                                  
1:
2:
3:
4:

Select allOpen in new window


The "static" specifier indicates that it is common code used by all instances of the object and that the code cannot directly access non-static members.  It is basically a global function, just like in Example 1; the calling convention is the same (no this pointer on the stack).

But what's the point of having a class member that can't access class members?

In this little base class, we have hard-coded the action that takes place on the timer event.  But you would certainly want to derive objects that could do different things.

{{{{ Example 2: Failing attempt to use a member function }}}} 

class CMsgTickTimer: public CBaseTimer {
public:
    CMsgTickTimer( int nIntervalMs, LPCSTR szMsg ) {
        m_nIntervalMs= nIntervalMs;
        strncpy( m_szMsg, szMsg, sizeof(m_szMsg) );
    }
    char m_szMsg[100];
    static void CALLBACK MyTimerProc( UINT, UINT, DWORD, DWORD, DWORD ) {
        printf( "------ %s ------- \r\n", m_szMsg ); // <<-- ERROR
    }
};
                                  
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:

Select allOpen in new window


The MyTimerProc function is attempting to access m_szMsg, a per-instance (non-static) value, but it is trying to do that in a static member function.  And that's not allowed.  Thus, the compiler error:

    error C2597: illegal reference to data member 'CMsgTickTimer::m_szMsg' in a static member function

The solution is to provide a this pointer to the callback function.  If you look at the API functions that use callbacks, they always provide a DWORD userValue that will be passed, unmolested, to the callback.  With a small change we can generalize this timer object so that any object derived from it will have an OnTimer() function that can do whatever is needed when the timer interval is up.

{{{{ Example 3:  Final version, allows access to derived object members }}}} 

#include <windows.h>
#include <stdio.h>
#include <Mmsystem.h>
#pragma comment(lib, "Winmm.lib" )

class CBaseTimer {
public:
    CBaseTimer(int nIntervalMs=1000) { m_nIntervalMs=nIntervalMs; }; // ctor
    ~CBaseTimer()               { Kill(); };                         // dtor
       int m_nID;
       long m_nIntervalMs;

    static void CALLBACK MyTimerProc( UINT uID, UINT uMsg, DWORD dwUser, DWORD dw1, DWORD dw2 ) {
        CBaseTimer* pThis= (CBaseTimer*)dwUser;
        pThis->OnTimer();
    }

    void Start() {  // TBD: Check if 0 (failed)
        m_nID= timeSetEvent( m_nIntervalMs, 10, MyTimerProc, (DWORD)this, TIME_PERIODIC ); //<<--- Note: this
    }
    void Kill()  {
        ::timeKillEvent( m_nID );
    }
    virtual void OnTimer()=0;  // must supply a fn in derived objects
};

//------------------------------------------ derived from the base class
class CMsgTickTimer: public CBaseTimer {
public:
    CMsgTickTimer( int nIntervalMs, LPCSTR szMsg ) {
        m_nIntervalMs= nIntervalMs;
        strncpy( m_szMsg, szMsg, sizeof(m_szMsg) );
    }
    char m_szMsg[100];
    void OnTimer() {
        printf( "---------- %s ----------- \r\n", m_szMsg );
    }
};

//------------------------------------------------ example of usage
int main(int argc, char* argv[])
{
    CMsgTickTimer cTimer( 500, "1/2 second is up" );
    cTimer.Start();
    for (int j=0; j< 40; j++) {
        Sleep(100);
        printf( "Hi there\r\n" );
    }
    return 0;
}
                                  
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:
39:
40:
41:
42:
43:
44:
45:
46:
47:
48:
49:
50:
51:
52:

Select allOpen in new window


The output shows that the OnTimer() fn of the derived object is called periodically.

CallBack-Article-Shot-1.JPG
  • 32 KB
  • Program Output
Program Output


So, what does this have to do with CreateThread, EnumWindows, and the other API functions named above?   Just this:  The same technique can be used with all of them.

CreateThread( prSecurity, nStackSize, pFn, pParam ... )
     ThreadProc( pParam) // will be your this pointer

AfxBeginThread( pFn, param .....)
     MyThreadProc( param ) // param will be your this pointer

EnumWindows( pFn, lParam)
     EnumWindowsProc( hWnd, lParam ) // lParam will be your this pointer

CreateWindowEx(.... lParam )
     WindowProc( hWnd, WM_CREATE, wParam, lParam) // lParam will be your this pointer

LVM_SORTITEMS (message sent to ListView control),  WPARAM is the user value
  MyCompareFunc( LPARAM p1, LPARAM p2, LPARAM lParamSort);  // last one is your this pointer

One other note.  You can use the passed-in parameter directly, for instance:

MyThreadProc( LPVOID p ) 
{
	CSomeObject* pThis= (CSomeObject*)p;
	printf("Member string variable is %s", pThis->m_sSomeMember );
}
                                  
1:
2:
3:
4:
5:

Select allOpen in new window


But the technique used in Example 3 is much more useful.  It uses pThis to call into a function of the object.   That way, execution "gets into" the object and from that point on, your code can access members normally.

UINT MyObjectThreadProc( LPVOID pParam ) {
	MyObject* pThis= (MyObject*)pParam;
	UINT nRet= pThis->Run();  // The thread does all the work in here
	return( nRet );
}
                                  
1:
2:
3:
4:
5:

Select allOpen in new window


=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
If you liked this article and want to see more from this author,  please click the Yes button near the:
      Was this article helpful?
label that is just below and to the right of this text.   Thanks!
=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=

    Comments

    Expert Comment

    by: pgnatyuk on 2009-05-29 at 12:23:14ID: 1207

    Small comment about CreateWindow - there is SetWindowLong function that allows to replace the Window procedure.  I know this way with SetWindowLong does not work WM_CREATE message.
    A lot of GUI libraries (including MFC) use the way you explained. This approach allows to use virtual functions and make class hierachies for the GUI controls.
    If it's allowed to me, I'd like to say "thanks, good job and nice article."

    Add your Comment

    Please Sign up or Log in to comment on this article.

    Loading Advertisement...

    Top C++ Experts

    1. jkr

      81,044

      Master

      0 points yesterday

      Profile
      Rank: Savant
    2. sarabande

      48,120

      0 points yesterday

      Profile
      Rank: Sage
    3. Zoppo

      23,770

      0 points yesterday

      Profile
      Rank: Genius
    4. evilrix

      16,916

      30 points yesterday

      Profile
      Rank: Genius
    5. Infinity08

      15,172

      0 points yesterday

      Profile
      Rank: Genius
    6. ambience

      12,888

      0 points yesterday

      Profile
      Rank: Wizard
    7. duncan_roe

      8,800

      0 points yesterday

      Profile
      Rank: Genius
    8. TommySzalapski

      8,800

      0 points yesterday

      Profile
      Rank: Genius
    9. satsumo

      5,800

      0 points yesterday

      Profile
      Rank: Guru
    10. gtokas

      5,600

      0 points yesterday

      Profile
      Rank: Wizard
    11. masheik

      4,572

      0 points yesterday

      Profile
      Rank: Guru
    12. kuroji

      4,000

      0 points yesterday

      Profile
    13. pepr

      4,000

      0 points yesterday

      Profile
      Rank: Genius
    14. AndyAinscow

      3,332

      0 points yesterday

      Profile
      Rank: Genius
    15. tampnic

      3,332

      0 points yesterday

      Profile
      Rank: Master
    16. phoffric

      3,200

      20 points yesterday

      Profile
      Rank: Genius
    17. navneethegde

      2,900

      0 points yesterday

      Profile
      Rank: Wizard
    18. Orcbighter

      2,832

      2,000 points yesterday

      Profile
      Rank: Master
    19. arnold

      2,800

      0 points yesterday

      Profile
      Rank: Genius
    20. mrwad99

      2,800

      0 points yesterday

      Profile
      Rank: Wizard
    21. giltjr

      2,800

      0 points yesterday

      Profile
      Rank: Genius
    22. ravenpl

      2,800

      0 points yesterday

      Profile
      Rank: Genius
    23. for_yan

      2,800

      0 points yesterday

      Profile
      Rank: Genius
    24. ubound

      2,550

      0 points yesterday

      Profile
      Rank: Master
    25. trinitrotoluene

      2,532

      0 points yesterday

      Profile
      Rank: Guru

    Hall Of Fame