CALLBACKS IN C++ USING A BASE CLASS WITH VIRTUAL FUNCTIONS

Introduction

Designing an application with multiple asynchronous events can be difficult.  An application which requires user input along with a network connection is an example of this type of application.  A mechanism must be found what can retrieve information from the user interface and transmit the data out of the network connection.  Also network data must be passed to the user interface.  The two major ways of accomplishing this task is polling and callbacks.

 

In the polling method, the process is always executing first calling the network class to see if there is data, then calling the user interface to see if work needs to be done.  This is inefficient because most of the time nothing will need to be done.

 

In the callback method, each class calls a function in a higher level class when something happens.  There are many ways to implement a callback.   This document will describe one method of doing callbacks.

 

An implementation of callbacks should meet the following criteria:

  1. Type Save    - the compiler should do all the type checking.  There should be no
  2. Non Coupling
  3. Generic
  4. Be able to call more than one callback

Example

As a simplified example, two classes will be defined: C1 and C2.  Class C1 will generate data which must be transferred to class C2.

 

The C2 class has three methods: call1, call2 and call3.  These methods print the values passed to the method and return the number of characters printed.

 

class C2

{

  public:

    int call1 (int t)

    {

      return printf ("c2::callback (%d)\n", t);

    }

    int call2 (int t, char *str)

    {

      return printf ("c2::callback (%d, %s)\n",

                      t, str);

    }

    int call3 (int t, char *str, int *rt)

    {

      return printf (

     "c2::callback (%d, %s, %d)\n", t, str, *rt);

    }

};

 

The C1 class has one method called function.  This function invokes three callback functions (callback1, callback2, callback3) and returns the sum of the return values of the callbacks.

 

There are also three callback declarations (SIGNAL1, SIGNAL2, and SIGNAL3).  The implementation of these declarations will be described later.  The SIGNAL1 declaration defines a method called callback1 which takes an integer as an argument and returns an integer value.  The SIGNAL3 declaration defines a method called callback3 which takes an integer, a character pointer and an integer pointer as arguments and returns an integer value.  There can be more that one SIGNAL1 declaration.

 

typedef char*       PCHAR;

typedef int*        PINT;

 

class C1

{

  public:

    int function (int test)

    {

      int rt;

      char buffer[256];

 

      sprintf (buffer, "string %d", test);

 

      rt = callback1 (test);

      rt += callback2 (test, buffer);

      rt += callback3 (test, buffer, &rt);

 

      return rt;

    }

 

    SIGNAL1 (int, callback1, int);

    SIGNAL2 (int, callback2, int, PCHAR);

    SIGNAL3 (int, callback3, int, PCHAR, PINT);

};

 

The main routine instantiates classes C1 and C2 and binds them together.  The main subroutine calls three connect macros: CONNECT1, CONNECT2, and CONNECT3.  These three macros bind the two classes together.

 

The CONNECT3 macro defines a connection from class c1, method callback3 which has arguments consisting of an integer value, a character pointer, and integer pointer which returns an integer value.  The method which will be called is in class c2 , function call3, which is of type C2.  An Identifier is returned so that the callback can be removed.

 

 

int main (int argc, char *argv[])

{

  C1  c1;

  C2  c2;

  int rt;

  CallbackId id1;

  CallbackId id2;

  CallbackId id3;

 

  CONNECT1 (&id1, int, &c1, callback1, int,

                   C2, &c2, call1);

  CONNECT2 (&id2, int, &c1, callback2, int, PCHAR,

                            C2, &c2, call2);

  CONNECT3 (&id3, int, &c1, callback3, int, PCHAR,

                          PINT, C2, &c2, call3);

 

  rt = c1.function (12345678);

 

  printf ("c1.function returns %d\n", rt);

 

  DISCONNECT1 (&c1, callback1, int, id1);

  DISCONNECT2 (&c1, callback2, int, PCHAR, id2);

  DISCONNECT3 (&c1, callback3, int, PCHAR, PINT, id3);

}

 

The output looks like this:

c2::callback (12345678)

c2::callback (12345678, string 12345678)

c2::callback (12345678, string 12345678, 65)

 

As you can see, call the function method in c1 caused the callback methods in c2 to be called.

Calling Class Implementation

Now let’s take a look at callback.h file.  This is where all the magic happens.  We’ll only discuss the callback functions with one parameter, the others are very similar.

 

The first class we’ll look at is the CallbackFunc1 class.

 

template <class RT, class P1>

class CallbackFunc1

{

  public:

    class CallbackFunc1 *next;

    virtual RT func (P1 t) = 0;

};

 

This is a template class which defines the method “func”.  The return type and the parameter types are defined with the class is instantiated.  The method “func” is a pure virtual function.  A next pointer is also provided so that a list can be created to call more than one method.

 

The next class is Callback1a.  A variable of Callback1a will be created in the class generating the callback by the macro SIGNAL1.

 

template <class RT, class P1>

class Callback1a

{

  private:

    CallbackFunc1<RT, P1> *func;

 

  public:

    Callback1a<RT, P1> ()

    {

      func = NULL;

    }

    ~Callback1a<RT, P1> ()

    {

      CallbackFunc1<RT, P1> *p;

      CallbackFunc1<RT, P1> *pNext;

 

      for (p = func; p != NULL; p = pNext)

      {

        pNext = p->next;

 

        delete p;

      }

    }

    void setFunc (CallbackFunc1<RT, P1> *f)

    {

      CallbackFunc1<RT, P1> *p;

 

      if (func == NULL)

      {

        f->next = NULL;

        func = f;

      }

      else

      {

        for (p = func; p->next != NULL; p = p->next)

        {

          continue;

        }

       

        f->next = NULL;

        p->next = f;

      }

    }

    RT callback(P1 i1)

    {

      CallbackFunc1<RT, P1> *p;

      RT rt (-1);

 

      if (func == NULL) return rt;

 

      rt = 0;

 

      for (p = func; p != NULL; p = p->next)

      {

        rt |= p->func (i1);

      }

 

      return rt;

    }

};

 

This class has one private member “CallbackFunc1 func”.  The “func” variable defines a list of class pointers.

 

This class defines four methods: constructor, destructor, setFunc and callback.  The constructor initializes the “func” class variable to NULL.  The destructor deletes the “func” member variable list; this means that the “func” member variable must be created using “new”.

 

The “setFunc” method adds the callback function to the list. The “callback” method calls the methods, defined by the CallbackFunc1 class, in the list.

 

Nothing in these two classes requires any knowledge of the class which will be called back.  These classes can be included in the calling class without knowledge of the callback class.

 

The SIGNAL1 macro binds the calling class, the CallbackFunc1 class and the Callback1b class together.  Here’s what it looks like with the line continuation removed:

 

#define SIGNAL1(rt,f,p1)

private:

  Callback1a<rt, p1> callback_##f##;

public:

  int connect_##f## (

               class CallbackFunc1<rt, p1> *p)

  {

    callback_##f##.setFunc (p);

  }

protected:

  int f (p1 p)

  {

    return callback_##f##.callback (p);

  }

 

The macro creates a Callback1a member variable, a connect function, and a callback function.

 

Connect Implementation

The CONNECT1 macro called in the main function is what binds the classes together.  Before we look at the implementation of the CONNECT1 macro, let’s look at other class: Callback1b.

 

template <class RT, class P1, class C>

class Callback1b : public CallbackFunc1<RT, P1>

{

  typedef RT (C:: *SlotFunc)(P1);

 

  private:

    C *c;

    SlotFunc slot;

 

  public:

    Callback1b (C *cc, SlotFunc s)

    {

      c = cc;

      slot = s;

    }

    RT func (P1 t)

    {

      return (c->*slot) (t);

    }

};

 

This class is based on the CallbackFunc1 class, we’ll see why later. This class has two member variable: c and slot.  These members define the callback class and callback function.  The constructure initializes these variables.  The “func” method overrides “func” method in the CallbackFunc1 class.  This part of the class to passed to the calling class.

 

This is accomplished by the CONNECT macro.

 

#define CONNECT3(RT,C1,signal,T1,CT2,C2,slot)

{

  Callback1b<RT, T1, CT2> *cb;

  cb = new Callback3b<RT, T1, CT2>

                               (&C2, &CT2::slot);

  C1.connect_##signal (cb);

}

 

This macro creates the Callback1b class and sets the callback class and the callback function.  The Callback1b class is then passed to the calling class.

How Does It Work?

Now that we have seen all the pieces, how does it work?  We create a new function which overrides a virtual function.  The new function is called instead of the virtual function.  The virtual function.

 

When the calling class is created, a Callback1a class for each callback is also created.  This Callback1a will be set initialized to zero.  When the Callback1a::callback method is called, nothing happens; the method simply returns an error.

 

When the main program calls the CONNECT macro is called, a Callback1b class is created.  The Callback1b class is a CallbackFunc class with a real “func” method instead of the virtual “func” method.  The CONNECT1 macro passes the CallbackFunc portion of the Callback1b class to the calling class containing the new “func” method.

 

Now, we’ll go back to the calling class.  When the Callback1a::callback method is now called, the initialized member variable is set to one.  The method calls the “func” method of the CallbackFunc class.  The method that will be called is the “func” method of the Callback1b class, which calls the callback function in C2.

 

 

Here’s the link to the actual header file.

Here’s the test file.

 

I’d appreciate any comments you might have, send me an email Uve Rick