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:
Functor(T *pObj, int (T::*pFunc)(int))
{
m_pObject = pObj;
m_pFunction = pFunc;
}
int operator ()(int p)
{
return (m_pObject->*m_pFunction)(p);
}
private:
T *m_pObject;
int (T::*m_pFunction)(int);
};
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()
{
MyClass theClass;
Functor<MyClass> myFunc(&theClass, MyClass::Square);
int result = myFunc(5);
}
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.
class Functor
{
public:
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<class T>
class TemplateFunctor : public Functor
{
public:
TemplateFunctor(T *pObj, int (T::*pFunc)(int))
{
m_pObject = pObj;
m_pFunction = pFunc;
}
int operator ()(int p)
{
return (m_pObject->*m_pFunction)(p);
}
private:
T *m_pObject;
int (T::*m_pFunction)(int);
};
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);
}
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) \
\
class name##Functor \
{ \
public: \
virtual void operator () parmdecl = 0; \
}; \
\
\
template<class C> \
class name##TFunctor : public name##Functor \
{ \
public: \
\
name##TFunctor(C* pObj, void (C::*pFunc)parmdecl) \
{ \
m_pObj = pObj; \
m_pFunc = pFunc; \
} \
\
\
void operator ()parmdecl { (m_pObj->*m_pFunc)parmcall; } \
C *m_pObj; \
void (C::*m_pFunc)parmdecl; \
};
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):
class AddFunctor
{
public:
virtual void operator () (int p1, int p2) = 0;
};
template<class C>
class AddTFunctor : public AddFunctor
{
public:
AddTFunctor(C* pObj, void (C::*pFunc)(int p1, int p2))
{
m_pObj = pObj;
m_pFunc = pFunc;
}
void operator ()(int p1, int p2) { (m_pObj->*m_pFunc)(p1, p2); }
C *m_pObj;
void (C::*m_pFunc)(int p1, int p2);
};
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<class C> \
void Add(C *pObj, void (C::*pFunc)parmdecl); \
\
void Add(void (*pFunc)parmdecl); \
\
template<class C> \
void Remove(C *pObj, void (C::*pFunc)parmdecl); \
\
void Remove(void (*pFunc)parmdecl); \
\
\
void operator +=(Functor *pFunc); \
void operator +=(void (*pFunc)parmdecl); \
\
template<class C> \
void operator -=(TFunctor<C> *pFunc); \
void operator -=(void (*pFunc)parmdecl); \
\
\
void Invoke parmdecl; \
\
void operator ()parmdecl; \
\
private: \
\
std::vector<Functor*> m_pFuncs; \
\
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()
{
A a;
B b;
AddDelegate::Delegate del;
del += AddHandler(&a, A::Fun1);
del += AddHandler(&b, B::Fun1);
del += GlobalFunc;
del += A::StaticFunc;
del(4, 5);
cout << "[main] a.value = " << a.value << endl;
cout << "[main] b.value = " << b.value << endl;
del -= AddHandler(&a, A::Fun1);
del -= A::StaticFunc;
del(4, 5);
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