Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

.NET style delegates for VC++ 6

0.00/5 (No votes)
19 Aug 2003 2  
An implementation of synchronous .NET style delegates in non - .NET VC++ 6.

Contents

Introduction

In .NET, delegates are used to implement event handling. They allow a class to register an event handler, which is then called at a later time when a certain event occurs. In non-.NET C++, this is not an easy thing to do. Non-static member functions can be quite difficult to use as callbacks. The purpose of this article is to present a method whereby both static and non-static class member functions, as well as non-member functions can be used as callback functions. Type-safety is important in this implementation, and some features are left out in order to retain this.

What are delegates?

The .NET framework SDK defines delegates as follows:

"A delegate is a class that can hold a reference to a method. Unlike other classes, a delegate class has a signature, and it can hold references only to methods that match its signature. A delegate is thus equivalent to a type-safe function pointer or a callback."

A class that exposes a delegate allows other functions and classes to register event handlers with it. The class can then call the delegate, which would iterate through its list of handlers, and call them one-by-one, passing the information passed to the delegate. The class that exposes the delegate does not need to know anything about how many handlers are registered. It leaves this up to the delegate itself.

Description

An overview of functors

This implementation makes use of functors, or function objects. These allow a non-static class member function to be called in the context of a specific object. Templates are used to allow any class-types to be used. A basic functor definition is shown below:

template<class T>
class Functor
{
public:
    // Constructor takes the values and stores them
    Functor(T *pObj, int (T::*pFunc)(int))
    {
        m_pObject = pObj;
        m_pFunction = pFunc;
    }
    // Invokes the stored function
    int operator ()(int p)
    {
        return (m_pObject->*m_pFunction)(p);
    }
private:
    T *m_pObject;                // Pointer to the object
    int (T::*m_pFunction)(int);  // Pointer to the function
};

This functor object uses functions that take an int as a parameter and return an int. The operator () is the most important part of the functor. It allows the object to be used as if it were a function. It's job is to invoke the stored function pointer whenever it is called. To use this functor object, we'd use code similar to that below:

class MyClass
{
public:
    int Square(int p) { return p * p; };
};

void some_function()
{
    // Create a class to call in the context of
    MyClass theClass;
    // Create and initialise the functor object
    Functor<MyClass> myFunc(&theClass, MyClass::Square);
    // Call the functor using the overloaded () operator
    int result = myFunc(5);
    // result will hold the value 25
}

Note that invoking the functor object is almost the same as invoking the function itself, thanks to the overloaded () operator. I say "almost" because the object pointer is not used - it is stored inside the functor object.

Ok, that's very nice, but why would you want to use functors over the functions themselves? Good question. Functors become much more useful when you want to call functions that look the same (have the same parameters and return value) without knowing what class or object they belong to. Take the following code for example. I've split it into sections to make it easier to follow.

First is the abstract base class representing a functor that takes an int and returns an int. It contains one function - the () operator, so that the functor can be called without knowing its type.

// Abstract base class
class Functor
{
public:
    // Invoke the functor (no implementation here as it must be overridden)
    virtual int operator()(int) = 0;
};

Next is the template class that can be instantiated for any class type, assuming it has a function that takes an int and returns an int. It is derived from the abstract Functor class, so that a pointer to the specific function object can be passed wherever a pointer to the base class is expected, so that the functor can be called regardless of what class it refers to. Other than the base class and the name, this class is identical to the one presented above.

// Template functor
template<class T>
class TemplateFunctor : public Functor
{
public:
    // Constructor takes the values and stores them
    TemplateFunctor(T *pObj, int (T::*pFunc)(int))
    {
        m_pObject = pObj;
        m_pFunction = pFunc;
    }
    // Invokes the stored function (overrides Functor::operator ())
    int operator ()(int p)
    {
        return (m_pObject->*m_pFunction)(p);
    }
private:
    T *m_pObject;                // Pointer to the object
    int (T::*m_pFunction)(int);  // Pointer to the function
};

Next is a simple function that takes a pointer to a functor for a parameter and calls it. Note that it takes a parameter to the base class Functor rather than a pointer to the template class. This is needed because each variation of the template class based on the template parameter is actually a different type, and cannot be used directly if more than one type is to be supported.

int OperateOnFunctor(int i, Functor *pFunc)
{
    if(pFunc)
        return (*pFunc)(i);
    else
        return 0;
}

This is a simple class that contains a function that meets the requirements of our functor - taking an int and returning an int. Note that this member function also uses a member variable, to show that the callback functions are in fact called in the context of an object, so functors referenced to different instances of the same class will produce different results.

class ClassA
{
public:
    ClassA(int i) { m_Value = i; }
    int FuncA(int i)
    {
        return (m_Value - i);
    }
    int m_Value;
};

This is a simple program that creates two instances of a functor for the same class type, calls the functor on two different objects, and displays the results.

int main()
{
    ClassA a(20);
    ClassA b(10);

    TemplateFunctor<ClassA> functorA(&a, ClassA::FuncA);
    TemplateFunctor<ClassA> functorB(&b, ClassA::FuncA);

    cout << "a gives the value " << OperateOnFunctor(5, &functorA) << endl;
    cout << "b gives the value " << OperateOnFunctor(5, &functorB) << endl;

    return 0;
}

This will give the following output:

a gives the value 15
b gives the value 5

In this case, both functors were calling ClassA::FuncA but for different objects. A similar but different example is calling a function from different classes. Say we implemented ClassB like this:

class ClassB
{
public:
    ClassB(int i) { m_Value = i; }
    int FuncB(int i)
    {
        return (m_Value + i);  // + instead of -
    }
    int m_Value;
};

If we implement the main function like this, we'll get results that are a bit different:

int main()
{
    ClassA a(20);
    ClassB b(10);

    TemplateFunctor<ClassA> functorA(&a, ClassA::FuncA);
    TemplateFunctor<ClassB> functorB(&b, ClassB::FuncB);

    cout << "a gives the value " << OperateOnFunctor(5, &functorA) << endl;
    cout << "b gives the value " << OperateOnFunctor(5, &functorB) << endl;

    return 0;
}

This will give us the following results:

a gives the value 15
b gives the value 15

In this case, functorB was calling ClassB::FuncB, hence the result (10 + 5). Note that we passed both functors to the OperateOnFunctor() function in exactly the same way. This is made possible due to the abstract Functor base class.

Parameterising functors with macros

So functors can be pretty handy things, but it's a bit of a pain to have to rewrite the classes if different numbers of parameters or a different return type are required. However, this can be made a bit simpler by using the preprocessor. Some may argue that this is macro abuse, but it works nicely, and until templates allow us to change function prototypes, it's the only way.

Say we declare a macro as follows:

#define DECLARE_FUNCTOR(name, parmdecl, parmcall)                       \
    /* A function object base class for this parameter list */          \
    class name##Functor                                                 \
    {                                                                   \
    public:                                                             \
        virtual void operator () parmdecl = 0;                          \
    };                                                                  \
                                                                        \
    /* Template class derived from Functor for                          \
       make class-specific function objects */                          \
    template<class C>                                                   \
    class name##TFunctor : public name##Functor                         \
    {                                                                   \
    public:                                                             \
        /* Only constructor - stores the given data */                  \
        name##TFunctor(C* pObj, void (C::*pFunc)parmdecl)               \
        {                                                               \
            m_pObj = pObj;                                              \
            m_pFunc = pFunc;                                            \
        }                                                               \
                                                                        \
        /* Invokes the function object with the given parameters */     \
        void operator ()parmdecl    { (m_pObj->*m_pFunc)parmcall; }     \
        C *m_pObj;                  /* Object pointer */                \
        void (C::*m_pFunc)parmdecl; /* Method pointer */                \
    };

The three macro parameters are defined as:

  • name - The name of the functor. The text "Functor" is appended to this name for the base class, and "TFunctor" is appended for the template class
  • parmdecl - The declaration of the parameter lists for the () operators. The list must be enclosed in parentheses
  • parmcall - The list of parameters as passed to the contained method. The example below would help to explain the relationship between these two lists

An example usage of this macro is shown below:

DECLARE_FUNCTOR(Add, (int p1, int p2), (p1, p2))

This declares a functor called AddFunctor which takes two int parameters. The output of this macro would look like this (except it would actually all be on one line, and there would be no comments):

    /* A function object base class for this parameter list */
    class AddFunctor
    {
    public:
        virtual void operator () (int p1, int p2) = 0;
    };

    /* Template class derived from AddFunctor for
       make class-specific function objects */
    template<class C>
    class AddTFunctor : public AddFunctor
    {
    public:
        /* Only constructor - stores the given data */
        AddTFunctor(C* pObj, void (C::*pFunc)(int p1, int p2))
        {
            m_pObj = pObj;
            m_pFunc = pFunc;
        }

        /* Invokes the function object with the given parameters */
        void operator ()(int p1, int p2)    { (m_pObj->*m_pFunc)(p1, p2); }
        C *m_pObj;                  /* Object pointer */
        void (C::*m_pFunc)(int p1, int p2); /* Method pointer */
    };

As you can see, wherever name appeared, the text Add has been inserted, parmdecl has been replaced with (int p1, int p2) and parmcall has been replaced with (p1, p2). To illustrate the relationship between the parmdecl and parmcall parameters, look at the operator () method, firstly in the macro, and then the expanded version:

void operator ()parmdecl            { (m_pObj->*m_pFunc)parmcall; }
void operator ()(int p1, int p2)    { (m_pObj->*m_pFunc)(p1, p2); }

parmdecl is the declaration of the parameter list for the function, while parmcall is the parameter list that is passed to the contained function. Unfortunately, there is no way to auto-generate this using macros. It's a bit of a kludge, but it works and allows the functions to be type-safe.

Delegate implementation

The delegates are implemented similarly to functors, but they store a list of functors that are called when they are invoked, rather than only one function pointer. This means that multiple handlers can be stored and invoked when required. The class definition (without the code) is shown below. I have left out the definition of the functors because it is shown above. The functors are actually declared in this macro also, inside the namespace declaration.

#define DECLARE_DELEGATE(name, parmdecl, parmcall)                      \
namespace name##Delegate                                                \
{                                                                       \
    class Delegate                                                      \
    {                                                                   \
    public:                                                             \
        Delegate();                                                     \
        ~Delegate();                                                    \
                                                                        \
        /* Template function for adding member function callbacks */    \
        template<class C>                                         \
        void Add(C *pObj, void (C::*pFunc)parmdecl);                    \
        /* Add a non-member (or static member) callback function */     \
        void Add(void (*pFunc)parmdecl);                                \
        /* Template function for removing member function callbacks */  \
        template<class C>                                               \
        void Remove(C *pObj, void (C::*pFunc)parmdecl);                 \
        /* Removes a non-member (or static member) callback function */ \
        void Remove(void (*pFunc)parmdecl);                             \
                                                                        \
        /* Addition operators */                                        \
        void operator +=(Functor *pFunc);                               \
        void operator +=(void (*pFunc)parmdecl);                        \
        /* Subtraction operators */                                     \
        template<class C>                                               \
        void operator -=(TFunctor<C> *pFunc);                           \
        void operator -=(void (*pFunc)parmdecl);                        \
                                                                        \
        /* Calls all the callbacks in the callback list */              \
        void Invoke parmdecl;                                           \
        /* Calls all the callbacks in the callback list */              \
        void operator ()parmdecl;                                       \
                                                                        \
    private:                                                            \
        /* List of callback functions */                                \
        std::vector<Functor*> m_pFuncs;                                 \
        /* typedef'd iterator */                                        \
        typedef std::vector<Functor*>::iterator vit;                    \
    };                                                                  \
}

Some key points:

  • The delegate and functor classes are put in their own namespace, so that they are one manageable unit.
  • The functors are stored in an STL vector. The vector contains pointers to the Functor base class, so it can contain instances of the template functor class for any type. Also, not shown above, is another functor that was defined to be able to call non-member functions or static-member functions. It is functionally identical, except that it doesn't store an object pointer or expect the function to be part of a class.
  • There are two methods to cause the delegate to call all the functors - either the Invoke() method or the () operator. Both methods cause exactly the same effect, in fact the () operator calls Invoke() internally to do the work.
  • There are two methods of adding and removing callbacks from the delegate. Using the Add()/Remove() methods, or the +=/-= operators. Similarly to the Invoke()/operator () pair, the two methods are functionally identical - the operators directly call the non-operator methods. Both methods have two overloads, one for class member callbacks, and one for non-class member or static member callbacks.

Also not included above macro is a non-member function that is used for creating functors to be passed to the += and -= operators. This member function is not placed in the namespace with the classes, and is called the name passed to DECLARE_DELEGATE(), appended with Handler. For example:

DECLARE_DELEGATE(Add, (int p1, int p2), (p1, p2))

would make the function have the following prototype:

template<class C>
AddDelegate::TFunctor<C> *AddHandler(C *pObj, 
               void (C::*pFunc)(int p1, int p2));

Using the code

The best way to show how to use the code is to give an example. The following example defines a delegate that takes an int and a float as parameters. It defines two simple classes with a compliant function in each, and also uses a static function and a non-class-member function.

DECLARE_DELEGATE(Add, (int p1, float p2), (p1, p2))

class A
{
public:
    A() { value = 5; }
    virtual void Fun1(int val, float val2)
    {
        value = val*2*(int)val2;
        cout << "[A::Fun1] " << val << ", " << val2 << endl;
    }
    static void StaticFunc(int val, float val2)
    {
        cout << "[A::StaticFunc] " << val << ", " << val2 << endl;
    }
public:
    int value;
};

class B : public A
{
public:
    void Fun1(int val, float val2)
    {
        value += val*3*(int)val2;
        cout << "[B::Fun1] " << val << ", " << val2 << endl;
    }
};

void GlobalFunc(int val, float val2)
{
    cout << "[GlobalFunc] " << val << ", " << val2 << endl;
}

int main()
{
    // Create class instances
    A a;
    B b;
    // Create an instance of the delegate
    AddDelegate::Delegate del;
    // Add our handlers
    del += AddHandler(&a, A::Fun1); // or del.Add(&a, A::Fun1);
    del += AddHandler(&b, B::Fun1); // or del.Add(&b, B::Fun2);
    del += GlobalFunc;              // or del.Add(GlobalFunc);
    del += A::StaticFunc;           // or del.Add(A::StaticFunc);
    // Invoke the delegate
    del(4, 5);                      // or del.Invoke(4, 5);
    // Print the class values
    cout << "[main] a.value = " << a.value << endl;
    cout << "[main] b.value = " << b.value << endl;
    // Remove some of the handlers
    del -= AddHandler(&a, A::Fun1); // or del.Remove(&a, A::Fun1);
    del -= A::StaticFunc;           // or del.Remove(A::StaticFunc);
    // Invoke the delegate again
    del(4, 5);                      // or del.Invoke(4, 5);
    // Print the class values
    cout << "[main] a.value = " << a.value << endl;
    cout << "[main] b.value = " << b.value << endl;
    return 0;
}

This demonstrates most of the delegate operations, and will produce the following output:

[A::Fun1] 4, 5
[B::Fun1] 4, 5
[GlobalFunc] 4, 5
[A::StaticFunc] 4, 5
[main] a.value = 40
[main] a.value = 65
[B::Fun1] 4, 5
[GlobalFunc] 4, 5
[main] a.value = 40
[main] b.value = 125

The code uses the stl.h file written by Oskar Weiland to enable it to compile cleanly at warning level 4. This file is included in the zip file, and is available here. The downloadable code includes the delegate.h file, and the example program given above.

Class reference

Due to the code being customized by the DECLARE_DELEGATE() macro, I'll use <parameters> to represent that parameters that you passed.

Method: template<class C> void Delegate::Add(C *pObj, void (C::*pFunc)(<parameters>))
Description: Adds a callback function that is a non-static member function of a class. The member function must return void and take a parameter list that is the same as <parameters>.
Return value: void - nothing.
Parameters:
  • pObj - A pointer to the object to call the callback method in the context of.
  • pFunc - A pointer to the callback method to call.
Method: void Delegate::Add(void (*pFunc)(<parameters>))
Description: Adds a callback function that is either a static member function of a class or is not a class member function. The function must return void and take a parameter list that is the same as <parameters>.
Return value: void - nothing.
Parameters: pFunc - A pointer to the callback function to call.
Method: template<class C> void Delegate::Remove(C *pObj, void (C::*pFunc)parmdecl)
Description: Removes a callback function from the callback function list
Return value: void - nothing.
Parameters:
  • pObj - A pointer to the object that is being referred to.
  • pFunc - A pointer to the callback method being referred to.

These two parameters together specify the callback handler to be removed.

Method: void Delegate::Remove(void (*pFunc)parmdecl)
Description: Removes a callback function from the callback function list
Return value: void - nothing.
Parameters: pFunc - A pointer to the callback method being referred to.
Method: void Delegate::operator +=(Functor *pFunc)
Description: Adds a callback function that is a non-static member function of a class. The member function must return void and take a parameter list that is the same as <parameters>.
Return value: void - nothing.
Parameters: pFunc - A pointer to the functor to call. This should be created using the <name>Handler() function.
Method: void Delegate::operator +=(void (*pFunc)(<parameters>))
Description: Adds a callback function that is either a static member function of a class or is not a class member function. The function must return void and take a parameter list that is the same as <parameters>.
Return value: void - nothing.
Parameters: pFunc - A pointer to the callback function to call.
Method: void Delegate::operator -=(Functor *pFunc)
Description: Removes a callback function that is a non-static member function of a class.
Return value: void - nothing.
Parameters: pFunc - A pointer to the functor to remove. This should be created using the <name>Handler() function, and is deleted by the function.
Method: void Delegate::operator -=(void (*pFunc)(<parameters>))
Description: Removes a callback function that is either a static member function of a class or is not a class member function.
Return Value: void - nothing.
Parameters: pFunc - A pointer to the callback function to remove.
Method: void Delegate::Invoke(<parameters>)
Description: Calls all the callbacks in the callback list with the specified parameters.
Return Value: void - nothing.
Parameters: <parameters> - The parameters to pass to the callback functions, as specified in the parameter to DECLARE_DELEGATE().
Method: void Delegate::operator ()(<parameters>)
Description: Calls all the callbacks in the callback list with the specified parameters
Return Value: void - nothing.
Parameters: <parameters> - The parameters to pass to the callback functions, as specified in the parameter to DECLARE_DELEGATE().

To-do

  • Add a macro-parameterized class that supports return values, storing the return value for each functor so that it can be accessed later.
  • Add template classes that have a fixed number of parameters, e.g. a 1-parameter class, a 2-parameter class etc. This may or may not be done due to the large number of classes involved - separate classes have to be written for delegates that return values as opposed to those that do not.
  • Suggestions?

History

  • 19th August 2003 - Initial release

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here