Dr. Dobb's is part of the Informa Tech Division of Informa PLC

This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.


Channels ▼
RSS

C/C++

Associations in C++


AUG94: Associations in C++

Callback lists allow arbitrary objects to work together here

Dan is a software engineer with Hewlett Packard's Medical Products Group, working with GUIs and real-time applications. He can be reached at [email protected].


The decomposition of event-driven systems into object-oriented designs poses interesting questions. This is particularly true when resolving issues about the relationships between objects. Common questions include:

  • Which classes "know" about the relationships to other classes?
  • Should you create "manager" classes or "relationship" objects to handle the associations?
  • How are these associations implemented without unduly sacrificing the reusability of classes?
Built-in relationships always have the potential for limiting the reusability of a class, since relationships are what is most likely to be different in a different context needing the same class.

In an application modeling a car, for example, the program might create classes for the various objects that together make up a car: engine, transmission, wheels, brakes, and so on. In modeling the car, the associations between these objects are surely at least as important as their individual behavior. However, if intimate knowledge of these relationships is coded into these objects, they might be much more difficult to reuse in another application that does not need the same relationships.

We need methods that allow objects to interact with one another without hardcoding the relationships. In this article I'll present a method of implementing relationships in terms of the behavior inherent in an object, rather than the use of references to specific objects. This is achieved via callback lists.

A callback is a function that's registered at run time with a data structure or object. The object then calls the function when particular events occur. Usually, if an object has callback support, it maintains a list of such callback functions for each type of event. When the event occurs, all callback functions on the corresponding list are invoked, one at a time.

Maintaining callback lists is a powerful technique because it allows arbitrary objects to work together in a highly synchronized manner, without building in specific knowledge about one another. For example, in a real-time medical application, a system might use callbacks to maintain and update a display. The system might collect heart rate, blood pressure, respiration, and other vital signs at irregular intervals. Such a system might build container objects to store the various measurements. If these container objects invoked a callback list each time a new value was inserted into the container, then any number of callbacks could be registered to be notified when this event occurs. Perhaps one callback could update a heart-rate display; another could perform a calculation involving the new measurement; others could trigger alarms if the new value fell within critical parameters, and so on. The point is that any number of dependencies can be placed on the event; more importantly, these dependencies can be registered and deregistered at run time. By providing a mechanism whereby callback procedures can be registered with an object, other objects can be notified of various events that concern them.

You can use the C++ class that's the focus of this article to easily and quickly add callback lists to your own classes. This class takes care of all the work necessary to maintain the callback list and invokes the chain of callbacks when told to. It also keeps track of client data that must be passed to the callback function when invoked. Once the basic infrastructure is in place, adding callbacks to your objects becomes fairly simple.

Throughout this article, I'll use the term "client" to refer to any object, function, or subprogram that supplies the actual callback functions. A client is any entity that wishes to register a callback on an object. In contrast, "owner" objects are those that own the callback lists. The owner "yanks" the callback chain when a particular event occurs, thereby invoking the various callback functions registered by its clients.

Listing One is Callback.h, which defines the classes Callback and CBMgr. Listing Two is Callback.cpp, which is the implementation for these classes. Callback is a class that encapsulates individual callback functions. CBMgr is a class that manages a chain of callbacks, providing a method to invoke the entire chain of callbacks, as well as methods to add and remove callbacks from the chain. An owner class that wishes to make use of these to support callbacks must:

  • Include a private CBMgr data member for each callback chain it wishes to maintain.
  • Provide a method for client classes to register callback procedures. Since the work involved in this is mostly done by the supplied CBMgr class, this method can usually be written as an inline function with one statement.
  • Invoke the corresponding callback chain when the appropriate event occurs. This can usually be done with a single statement, since the work of traversing the chain and invoking the callbacks is the job of the CBMgr class.
  • Publish in the class interface, the format of the event data passed to the callbacks when they are invoked. The event data can range from a simple NULL value to a pointer to an arbitrarily complex structure.

Callback and CBMgr Classes

First of all, every Callback object contains a pointer to a function. This function is the one called when the callback is invoked. This function must have a particular prototype, which is also defined just above the class definition (see Listing One).

The second private data member of the Callback class is the client data supplied with the callback when it's registered. This data is simply held, and each time the callback is invoked, this item is supplied as the second parameter to the callback function. Its format and contents need be known only to the actual callback function.

The third private data member of the Callback class is the pointer to the owner object. It is supplied when the callback is created and is simply held until the callback is invoked. Each time the callback is invoked, this pointer is passed as the first parameter to the callback function. This parameter can be used by the callback function to perform additional actions on its owner object. For example, if an object with several interrelated attributes invokes a callback chain each time one of its attributes changes, then callbacks can be registered on the object to perform consistency checking. This guarantees that the attributes of the object will stay within certain parameters set by the callback function.

The fourth private data member of the Callback class is a pointer to the next callback. This simplifies building chains of callback objects. The usefulness of this member will become clearer when you meet the CBMgr class. This member can be queried and set using the GetNext() and SetNext() methods. If a container class library is available, this member can be done away with because all list manipulation can be encapsulated by the CBMgr class (using an abstract linked list from the container library). To keep things simple, however, I build the list directly.

The constructor for the Callback class requires three arguments: a pointer to the owner object, the pointer to the actual callback function, and the client data supplied by the client when the function was registered. Callback objects are never actually instantiated by clients; rather, they're created by the CBMgr object when its Register method is called.

The Invoke method is used to call the callback function. When Invoke is called, the supplied parameter is the call data, which is passed to the callback function as the third argument. (Invoke also passes the client data as well as the owner object pointer, both of which are supplied by the Callback object.) As with the constructor, this method is rarely called directly; rather, it is usually called by the CBMgr object when its own InvokeAll method is called.

As mentioned earlier, the prototype for callback functions is specified by the callback class. Its return type is always void, and it will expect these parameters:

Parameter 1 is a pointer to the object that owns the callback (that is, the object that this function is registered on). As mentioned earlier, this can be used by the callback function to perform additional actions on the owner object.

Parameter 2 is a client-defined data item that's passed in when the callback function is registered. This value is usually a pointer. The callback object holds on to the client data and supplies this item to the function call each time it's invoked. This client data can be set up prior to registering the callback to contain whatever data will be necessary for the callback to do its job. For example, in the medical application described earlier, the client data might be the window handle or display ID where the heart rate is to be displayed. Sometimes this parameter is unnecessary, in which case NULL can be supplied.

Parameter 3 is the event data, the data item supplied by the owner object. It's provided each time the callback is invoked, and is intended to convey state information or other relevant data regarding the event that caused the callback chain to be invoked. Typically this parameter is a pointer to a structure that contains information relevant to the event that invoked the callback. The container objects might have a callback chain invoked each time a new item (for example, a heart-rate value) is added to the container. When the callback chain is invoked, the event-data parameter points to the most recently added value. What this parameter contains is completely up to the owner class, but its format must be documented by the owner class so that clients that register callbacks know what to expect. As with the client-data parameter, if this data is deemed unnecessary by the owner class, it can pass NULL.

The CBMgr class maintains a list with an arbitrary number of Callback objects. It does the work of registering new callbacks and deregistering callbacks that need to be removed. Finally, it simplifies the task of calling all the callbacks on the chain, which is the most frequent action performed on a callback chain. With the CBMgr class, objects can invoke an entire chain of callbacks with a single function call requiring one argument.

Internally, the CBMgr object must maintain pointers to the first and last objects in the chain. (As mentioned earlier, if a linked-list container class is available, these pointers will be replaced by the linked-list container object.) The public methods are Register, Deregister, and InvokeAll. Register(void*pObj, PFNCB pfn, void*clientData) is the method used to add a callback to a CBMgr. Typically this method is called by the owner object (on behalf of the client) and returns a pointer to a Callback object, which should be passed back to the client registering the callback. The pointer can be used by the client to uniquely identify the callback when it needs to remove the callback.

Deregister(PCB pcb) is the counterpart of the register method and removes a callback from a CBMgr object. Again, this function is usually called by the owner of the callback chain on behalf of the client. This client must supply the pointer to the callback object it received when the callback was registered. You might be tempted to use a simpler method to uniquely identify the callback so as to not require the client to hold on to an identifier. One method is to use the address of the actual callback function. Simply using the address of the function to identify the callback would not be adequate, since in many circumstances a particular function could be registered several times on the same callback list (each instance with different client data). In effect, a callback is uniquely identified by a pair (function address and client data), encapsulated by the Callback object. Returning the pointer to the Callback object uniquely identifies the callback instance.

InvokeAll(void*eventData) is the method that the owner object calls when it wishes to "yank" a callback chain. When this function is called, the CBMgr object traverses its list, invoking the callbacks one by one and passing to each the eventData parameter. The eventData is formatted by the owner object, and may be as simple as a single discrete value or as complex as a pointer to an elaborate structure. In any case, it should not be unexpected by the callback function, since the format of the eventData should be published in the owner-class interface.

An Example

Listing Three is an example program which demonstrates the use of the callback classes. It defines a class, NumContainer, which will be given integers one at a time using the AddNumber method. Each time AddNumber is called, NumContainer will invoke the callback list.

After the program creates an instance of NumContainer, it registers four callbacks on the container (actually all four callbacks are the same function, but they have different clientData). The program then generates 100 random numbers, putting them into the NumContainer. Finally, the program prints the results. The count of numbers and the average are obtained by calling methods on NumContainer. The results of the callbacks can be observed by examining the clientData of the callbacks, which are in the global variable aCData (an array of structures, one for each callback). The output from a typical run is shown in Figure 1.

This example shows how to add callbacks to your own classes. Notice that adding callbacks to a class involves very little overhead. Both the Register and Deregister methods on NumContainer were implemented as a single-line inline function, and the AddNumber method had one extra statement to invoke the callback chain.

Conclusion

Once you begin to use callbacks, you'll find many uses for them. They're especially suited for configurable, event-driven systems that can change configuration during run time. Since associations can be added/removed at run time, they need not be hardcoded. Callbacks are also very useful in applications requiring "watchdogs" to monitor the value of certain parameters. These watchdogs can be installed (as callbacks) without disrupting the primary design of the system.

Another use is in the design of GUIs. If, for example, you'd like a certain label highlighted whenever a particular entry field receives the focus, a callback would do nicely. Just have the entry-field object invoke the "gaining focus" and "losing focus" callback chains. Register the callback functions for each of these. The "gaining focus" callback would highlight the associated label, while the "losing focus" callback could reset the label to its normal state. Using similar methods, callbacks can enforce many different types of dependencies between fields in dialogs and other user-interface code.

Figure 1: Output of C++ callback demonstration program (Listing Three).

There were 100 numbers generated.
The average is 52
There were 11 numbers greater than or equal to 90
There were 19 numbers greater than or equal to 80
There were 31 numbers greater than or equal to 70
There were 42 numbers greater than or equal to 60

Listing One

/***** Callback.h *****/

//----------------------------- defines ----------------------------
#define NULL    0L
#define TRUE    1
#define FALSE   0

//----------------------------- typedefs ---------------------------
typedef int    BOOL;        // define a Boolean type
typedef void FNCB(void * pObj, void * clientData, void * callData);
typedef FNCB * PFNCB;

//------------------------------ Class -----------------------------
class Callback {
    void *      cdata;          // client data
    PFNCB       pfnCallback;    // function to be called
    void *      pOwner;         // object that owns the callback
    Callback    *pNext;         // pointer to next callback in chain
public:
    Callback(void * pObj, PFNCB pfn, void * clientData);
    ~Callback();
    void        Invoke (void * callData)
                {      pfnCallback(pOwner, cdata, callData); };
    Callback *  GetNext ()
                {   return pNext;   };
    void        SetNext (Callback * pCB)
                {   pNext = pCB;    };
};
typedef Callback * PCB;
class CBMgr {
    Callback    *pFirst;    // pointer to first callback in chain
    Callback    *pLast;     // pointer to last callback in chain

    void AddToList (PCB pcb);      // method to add a callback to the list
    BOOL RemoveFromList (PCB pcb); // method to remove a callback from list
public:
    CBMgr();
    ~CBMgr();
    PCB Register (void * pObj, PFNCB pfn, void * clientData);
    BOOL Deregister (PCB pcb);
    void InvokeAll (void * callData);
};


Listing Two


/***** Callback.cpp *****/

//------------------------- Includes ------------------------------
#include "Callback.h"

//--------------------------- code --------------------------------
Callback::Callback(void * pObj, PFNCB pfn, void * clientData)
{    cdata = clientData;
     pfnCallback = pfn;
     pOwner = pObj;
     pNext = NULL;
}
Callback::~Callback()
{
}
CBMgr::CBMgr()
{    pFirst = NULL;
     pLast = NULL;
}
CBMgr::~CBMgr()
{    PCB     p, pNxt;
     p = pFirst;
     while (p) {                 // traverse list and destroy all
         pNxt = p->GetNext();    // callbacks that still remain
         delete (p);
         p = pNxt;
    }
}
void CBMgr::AddToList (PCB pcb)
{     if (pLast) {
         pLast->SetNext(pcb);
         pLast = pcb;
         }
      else {
         pLast = pcb;
         pFirst = pcb;
     }
}
BOOL CBMgr::RemoveFromList (PCB pcb)
{    PCB     p;
     BOOL    fFound = FALSE;
     p = pFirst;
     if (p == pcb) {
         pFirst = pFirst->GetNext();
         fFound = TRUE;
     } else
         while ((p) && !fFound)
             if (p->GetNext() == pcb) {
                 p->SetNext(pcb->GetNext());
                 fFound = TRUE;
             } else
                 p = p->GetNext();
     return fFound;
}
PCB CBMgr::Register (void * pObj, PFNCB pfn, void * clientData)
{
    PCB     pcb = new Callback(pObj, pfn, clientData);
    AddToList (pcb);
    return pcb;
}
BOOL CBMgr::Deregister (PCB pcb)
{
    if (RemoveFromList (pcb)) {
        delete (pcb);
        return TRUE;
    } else
        return FALSE;
}
void CBMgr::InvokeAll (void * callData)
{
    PCB     p;
    p = pFirst;
    while (p) {                 // traverse list
        p->Invoke (callData);   // invoking each callback in the list
        p = p->GetNext();
    }
}


Listing Three


/***** C++ callback demonstration program *****/

//-------------------------------- Includes ----------------------------
#include <stdio.h>
#include <stdlib.h>
#include <time.h>               // needed by randomize()
#include "Callback.h"

//-------------------------------- Defines -----------------------------
#define NCRITERIA          4        // number of callbacks we will register
#define NROUNDS            100      // number of random numbers generated
#define MAXNUM             100      // max range for random numbers

//--------------------------------- Types  -----------------------------
// The NumContainer class will be the 'owner' class in this example. Random
// numbers will be given to an instance of NumContainer. The numbers will be
// summed and counted. On arrival of each number, the NumContaier class will 
// also invoke the callback list. The eventData passed to the callbacks when 
// invoked will be the new number just added to NumContainer.
class NumContainer {
    int    Total;
    int    Count;
    CBMgr  CBList;
public:
    NumContainer();         // constructor
    void AddNumber (int num);
    int QueryAvg() { return (Total/Count); };
    int QueryCount() { return Count; };
    PCB RegisterCallback (PFNCB pfn, void * clientData)
        { return CBList.Register ((void *)this, pfn, clientData); };
    BOOL DeregisterCallback (PCB pcb)
        { return CBList.Deregister (pcb); };
};
// The structure defined below will be used by the callback functions as
// their clientData.  The structure will define the threshhold value, and
// a count of the numbers that are greater than or equal to the threshhold.
typedef struct {
    int  Threshold;
    int  Count;
} CRITERIA;

//------------------------------ Static Data ---------------------------
static CRITERIA aCData[NCRITERIA] = { {90,0}, {80,0}, {70,0}, {60,0} };

//------------------------------- Prototypes ---------------------------
// This is the function that will be registered as the callback. It will be 
// registered 4 different times, that is, there will be 4 instances of this 
// function registered, each with a different threshold value in the structure
// pointed to by cData.
void CounterCallback (void * pObj, void * cData, void * eventData);

//---------------------------------- Code ------------------------------
NumContainer::NumContainer()    // constructor for NumContainer
{
  Total = 0;
  Count = 0;
}
void NumContainer::AddNumber (int num)
{
  Total += num;
  Count++;
  CBList.InvokeAll((void *)num);
}
void CounterCallback (void * pObj, void * cData, void * eventData)
{
  CRITERIA  *pC = (CRITERIA*)cData;     // cast client data
  if ((int)eventData >= pC->Threshold)  // if new value >= threshold then
    pC->Count++;                        //    increment counter
}
void main ()
{
  NumContainer  nc;         // number container object
  int           i;
  // Register callbacks on the number container object.
  for (i=0; i < NCRITERIA; i++)
    nc.RegisterCallback (CounterCallback, &aCData[i]);
  // Generate random numbers, and add them to the number container. Each time 
  // we Add a new number, the entire chain of callbacks should be called.
  randomize();
  for (i=0; i < NROUNDS; i++)
    nc.AddNumber(random(MAXNUM));
  // Display results.
  printf ("There were %d numbers generated.\n",nc.QueryCount());
  printf ("The average is %d\n",nc.QueryAvg());
  for (i=0; i < NCRITERIA; i++)
    printf ("There were %d numbers greater than or equal to %d\n",
                                         aCData[i].Count, aCData[i].Threshold);
  exit(0);
}

Copyright © 1994, Dr. Dobb's Journal


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.