Click here to Skip to main content
15,885,890 members
Articles / Programming Languages / C++

Delegates in C++ with a natural assignment operator

Rate me:
Please Sign up or sign in to vote.
2.86/5 (7 votes)
14 Jul 2008CPOL8 min read 21K   260   7  
Delegate type just like in C# or Delphi.

About article example

The example in the attachment to this article is a very trivial example, but it meant for showing the simplicity in the usage of event classes and similarity to firing events in other programming languages. It contains a standard MFC dialog form with two button controls of type CBtnWithEvent. Each of buttons has a built-in Click event of type NotifyEvent which is the alias for QProcedure<CWnd*, int>. When you press any button, then its DoClick method will be invoked, the OnOkClick or OnCancelClick dialog method will be fired, and the program will be terminated.

Usage

It all begins in the CEventsDemoDlg::OnInitDialog method, where the buttons are attached to the HWND (it can be done in many ways; e.g., in the CDialog::DoDataExchange method) and the Click events are assigned to the handler methods on the dialog.

C++
// in header files
// typedef QProcedure<CWnd*, int> NotifyEvent;
// public: NotifyEvent CBtnWithEvent::Click
// public: CBtnWithEvent CEventsDemoDlg::buttonOK
// public: CBtnWithEvent CEventsDemoDlg::buttonCancel

BOOL CEventsDemoDlg::OnInitDialog() {
  /* */
  buttonOK.Attach(GetDlgItem(IDOK)->m_hWnd);
  buttonOK.Click = &CEventsDemoDlg::OnOkClick, this;

  buttonCancel.Attach(GetDlgItem(IDCANCEL)->m_hWnd);
  buttonCancel.Click = &CEventsDemoDlg::OnCancelClick, this;

  return TRUE;
}

Next, after pressing any of the buttons, its DoClick method is called from the overridden CButton::OnChildNotify method.

C++
void CButtonWithEvent::DoClick() {
  if (Click != NULL)
    Click(this, GetWindowLong(m_hWnd, GWL_ID));
}

In the end, a suitable method handler is called:

C++
void CEventsDemoDlg::OnOkClick(CWnd* sender, int nID)
{
  ASSERT( nID == IDOK );
  EndDialog(nID);
}
void CEventsDemoDlg::OnCancelClick(CWnd* sender, int nID)
{
  ASSERT( nID == IDCANCEL );
  EndDialog(nID);
}

Introduction

A number of years ago, I had the need to translate a database application written in Delphi to C++. The task was simple, but with one little detail, C++ has no built-in delegates. Of course, the C++ language provides pointers to methods, but invoking methods via pointers needs passing pointers to objects, for which these methods will be invoked. And in many cases, using method pointers is more flexible than applying them with whichever types without type casting. So I decided to write my own implementation of the delegate type. In this article, I will try to explain you how I did it.

File content

qevents.h presents four classes: QProcedure and QFunction being strictly delegate classes, their common ancestor - the QCustomEvent class, and the trivial QEException class to recognize exceptions from earlier mentioned classes. Both QProcedure and QFunction are template classes with zero up to ten default dummy types of _yryQQ6* parameters, which are substituted with the appropriate method parameters at compilation time. All these classes have been assembled in the properties namespace.

The 'this' pointer

The this pointer is very important when methods are invoked, because it is the default context for class fields and other methods called from inside their bodies. But we can very easily change this pointer, because the compiler only checks the type for which the method will be invoked. For example:

C++
class SomeClass
{
  int m_field;
  void f(void*) { }
  int g(int, int, int) { return 0; }
};

class AnotherClass {
  int m_field;

public:
  void f(void* param)
  {
    bool this_is_not_null = (this != NULL);
    bool this_like_param = (this == param);

    m_field = 0; // - when param is AnotherClass* then is natural
                 // - when param is SomeClass* then is ok, because
                 //   SomeClass has field with int width at the same offset
                 // - when param is NULL then will be boom
  }

  void g()
  {
    SomeClass sc;
    AnotherClass ac;
    void (AnotherClass::*meth)(void*) = &AnotherClass::f;
    // 1
    (this->*meth)(this); // is ok
    // 2
    (&ac->*meth)(&ac); // is ok also
    // 3
    (&sc->*meth)(&sc); // error: &cs is not of AnotherClass*
    // 4
    (((AnotherClass*)&sc)->*meth)(this); // dangerous
    // 5
    (((AnotherClass*)0)->*meth)(this); // dangerous
  }
};

The above example shows for invoking a method via a pointer, the compiler strongly checks only the method signature; however, we can easily cheat the pointer to the object for which this method will be invoked by simply type casting to the required type. In AnotherClass::f, for the first and second calls, both this_is_not_null and this_like_param will be true, but yet in the fourth call, this_like_param will be false, and in the fifth, the this pointer will be NULL. The code for each method is in reality embedded in the running application. These are not dynamic methods, each method has exactly one block of binary code during execution regardless of the number of instances of the types where these have been implemented. We only change their context (this pointer) when they will be invoked.

Changing the type of pointer to method

It is not just the this pointer that can be changed via type casting in C++. I could to say that verbally everything can be. What is most interesting for us is, type of pointers to methods. In the previous example, the third call in AnotherClass::g causes a compilation error, but it would be written as follows:

C++
// previous code from AnotherClass::g()
// (&sc->*meth)(&sc);  // error: &cs is not of AnotherClass*

// with type casting now is available
(&sc->*( (void (SomeClass::*)(void*)) meth ))(&sc);

Of cource, this is a not a very readable syntax; the one below will be more readable:

C++
typedef void (SomeClass::* SCM)(void*);
typedef void (AnotherClass::* ACM)(void*);

void f()
{
  SomeClass sc, *psc = &sc;

  ACM ac_meth = &AnotherClass::f;

 (psc->*((SCM)ac_meth))(psc);  
}

There is still one danger with type casting of method pointers. Let us consider the following situation:

C++
void g()
{
  SomeClass sc, *psc = &sc;

  int (SomeClass::* g_ptr)(int,int,int) = &SomeClass::g;

  (psc->*((SCM)g_ptr))(psc);
}

On the first look, all is OK. But, when the flow control reaches calling g_ptr, then it will be invoked with SCM semantic. The result of SomeClass::g does not have much meaning. The error is somewhere else, because this method needs three arguments and it obtained only one, and a stack unwind problem occurs. What exactly is going on during this calling is the topic for another article, so I won't dwell on it now. We must remember that tricks like this are very dangerous and always cause errors at runtime.

Method templates

Let us look to the right side of the assignment to the method pointer variable for a minute. Without type casting, it still causes a compilation error and the casting must be implicitly done - more comfortable.

C++
SCM sc_meth = &AnotherClass::f; // causes compilation error
SCM sc_meth = (SCM)&AnotherClass::f; // is ok

But, what will happen when we use the template method with the pointer to the method parameter of the unnamed class? It could be written like:

C++
class AnotherClass
{
/* */
public:
  template<typename CLASS> void h( void (CLASS::*)(void*) ) { }
}

The following syntax can be used now:

C++
void i()
{
  AnotherClass ac;
  SCM sc_meth = &SomeClass::f;
  ACM ac_meth = &AnotherClass::f;

  ac.h(sc_meth);
  ac.h(ac_meth);
}

In both of the above, when calling AnotherClass::h, the compiler recognizes the type CLASS, which owns the method with no result and a parameter of type void*, and in the first, it will be the AnotherClass class, and in the second, it will be SomeClass. We have defined the AnotherClass::h template method with a parameter for a method pointer with an implicit result type and an implicit parameter. After little rebuilding our method, it can take a parameter of any type; the change relies on adding two template parameters: one for the result type and one for the method parameter type. Here is the code:

C++
class AnotherClass
{
/* */
public:
  template<class RES, class CLASS, class PAR>
    void j(RES (CLASS::*mptr)(PAR)) { }

  void k()
  {
    SCM sc_meth = &SomeClass::f;
    ACM ac_meth = &AnotherClass::f;

    j(sc_meth);
    j(sc_meth);
  }
}

It seems that we gain nothing. But it is just the opposite. In the body of AnotherClass::j, we have full information about the mptr method pointer: its result type in the RES template parameter, the type of its parameter in PAR, and the type of the class which owns it in CLASS.

Class templates

With method templates goes class templates. Thanks to class templates, we are able to implement behaviour for types, which we don't know yet. Class templates like method templates can have one or more template parameters, but unlike method templates, these parameters can be default. Now, I will show how to gain the same result like with the AnotherClass::j method using class templates. I improvised the previous example:

C++
template<typename RES, typename PAR>
  class MethodWithOneParameterHandler
{
  typedef RES (MethodWithOneParameterHandler::*MPTR)(PAR);
  MPTR m_mptr;

public:
  template<typename CLASS> const MethodWithOneParameterHandler&
    operator=(RES (CLASS::*mptr)(PAR)) {
    m_mptr = (MPTR)mptr;
    return *this;
  }
};
/* */
void l()
{
  MethodWithOneParameterHandler<void, void*> handler;
  handler = &SomeClass::f;
  handler = &AnotherClass::f;
}

The simple MethodWithOneParameterHandler class implements one very important thing, namely, it omits and simultaneously hides information about the class which owns assigning method pointers to it. In real delegates, this information is transparent, and we don't need to worry about it. We need to remember assigning the method pointer value in our field (here, it is m_mptr) using simple type casting.

In the example, the result type of the method is void, which cannot be a return type. But one word of explaining. Class templates and method templates are treated as self parameters, so in this meaning, void has the same rights as every other type. From the definition of the delegate, the type is known and the parameters can be variable depending on the delegate type. In order to grow the list, the examined types only need to add the target number of parameters and add the overloaded version of the assignment operator for each of parameters. In order to make this class and the final result our delegate independent from its types, we only need to give each parameter its default value, that is the type name. This default type might be any type, e.g., int or double, but it is best when it is a pointer type. Here is the idea illustration:

C++
typedef struct IMAGINE_TYPE { int unused; } *IMGT;

template<class RES, class P1=IMGT, class P2=IMGT, class P3=IMGT>
  class MethodWithMaxThreParsHandler {
  typedef RES (MethodWithMaxThreParsHandler::*MPTR_P0)();
  typedef RES (MethodWithMaxThreParsHandler::*MPTR_P1)(P1);
  typedef RES (MethodWithMaxThreParsHandler::*MPTR_P2)(P1,P2);
  typedef RES (MethodWithMaxThreParsHandler::*MPTR_P3)(P1,P2,P3);

  union { MPTR_P0 m0; MPTR_P1 m1; MPTR_P2 m2; MPTR_P3 m3; } m_mptr;

  int parameter_count;

public:
  template<typename CLASS> const MethodWithMaxThreParsHandler&
    operator=(RES (CLASS::*mptr)()) {
    m_mptr.m0 = (MPTR_P0)mptr;
    parameter_count = 0;
    return *this;
  }

  template&lttypename CLASS> const MethodWithMaxThreParsHandler&
    operator=(RES (CLASS::*mptr)(P1)) {
    m_mptr.m1 = (MPTR_P1)mptr;
    parameter_count = 1;
    return *this;
  }

  template<typename CLASS> const MethodWithMaxThreParsHandler&
    operator=(RES (CLASS::*mptr)(P1, P2)) {
    m_mptr.m2 = (MPTR_P2)mptr;
    parameter_count = 2;
    return *this;
  }

  template<typename CLASS> const MethodWithMaxThreParsHandler&
    operator=(RES (CLASS::*mptr)(P1, P2, P3)) {
    m_mptr.m3 = (MPTR_P3)mptr;
    parameter_count = 3;
    return *this;
  }
};

After calling the assignment operator, we have the ability to check which of the overloaded operators was called, so mptr is appropriately cast and the actual number of method pointer parameters is stored in the parameter_count field. For cost decrementing, m_mptr is the union, because pointers MPTR_P<0|1|2|3> have the same size. The class prepared like above can be used as follows:

C++
class SomeClass {
/* */
public:
  char* m_with_1_par(char) { return NULL; }
  long m_with_3_par(short, int, long) { return 0; }
};

class AnotherClass {
/* */
public:
  int m_with_0_par() { return 0; }
  double m_with_2_par(float, double) { return 0.0; }
};

void m()
{
  MethodWithMaxThreParsHandler<int> with_0_parameters;
  MethodWithMaxThreParsHandler<char*, char> with_1_parameters;
  MethodWithMaxThreParsHandler<double, float, double> with_2_parameters;
  MethodWithMaxThreParsHandler<long, short, int, long> with_3_parameters;

  with_0_parameters = &AnotherClass::m_with_0_par;
  with_1_parameters = &SomeClass::m_with_1_par;
  with_2_parameters = &AnotherClass::m_with_2_par;
  with_3_parameters = &SomeClass::m_with_3_par;
}

Passing a pointer to object

There are many ways to pass a pointer to an object for which its events will be fired. It can be simply passing a pointer to an object in a method parameter or assigning it to some delegate field, but these operations need additional calling. In order to do an one-line assignment, it can be any operator with two parameters, where the first will be a delegate type, for example, a comma operator. Here is a sample code:

C++
template<class RES, class P1=IMGT, class P2=IMGT, class P3=IMGT>
  class MethodWithMaxThreParsHandler {
/* */
private:
  MethodWithMaxThreParsHandler* _p_this;
  MethodWithMaxThreParsHandler* get_this() {
    if (_p_this == NULL)
      throw;
    return _p_this;
  }

public:
  MethodWithMaxThreParsHandler() {
    _p_this = NULL;
    parameter_count = (-1);
  }

  friend void operator,(const MethodWithMaxThreParsHandler &e, void* p) {
    ((MethodWithMaxThreParsHandler*)&e)->_p_this = (MethodWithMaxThreParsHandler*)p;
  }
};

Here, in the MethodWithMaxThreParsHandler class modification, the pointer to the object passed through the comma operator is stored in the private _p_this field. This pointer will serve us later as the this pointer passed to the handler method during firing of events, so to start, let its value be NULL for later checking in the get_this method.

C++
void n()
{
  SomeClass sc;
  AnotherClass ac;

  MethodWithMaxThreParsHandler<int> with_0_parameters;
  MethodWithMaxThreParsHandler<char*, char> with_1_parameters;
  MethodWithMaxThreParsHandler<double, float, double> with_2_parameters;
  MethodWithMaxThreParsHandler<long, short, int, long> with_3_parameters;

  with_0_parameters = &AnotherClass::m_with_0_par, &ac;
  with_1_parameters = &SomeClass::m_with_1_par, &sc;
  with_2_parameters = &AnotherClass::m_with_2_par, &ac;
  with_3_parameters = &SomeClass::m_with_3_par, &sc;
}

Firing events

In order to add the possibility for firing events with functions, the best option is the function operator. This operator, as well as each of the methods, can be overloaded. Thanks to it, we can invoke any of the methods earlier assigned to the delegate variable.

C++
template<class RES, class P1=IMGT, class P2=IMGT, class P3=IMGT>
  class MethodWithMaxThreParsHandler {
/* */
public:
  RES operator()() {
    if (parameter_count != 0) throw;
    return (get_this()->*m_mptr.m0)();
  }

  RES operator()(P1 p1) {
    if (parameter_count != 1) throw;
    return (get_this()->*m_mptr.m1)(p1);
  }

  RES operator()(P1 p1, P2 p2) {
    if (parameter_count != 2) throw;
    return (get_this()->*m_mptr.m2)(p1, p2);
  }

  RES operator()(P1 p1, P2 p2, P3 p3) {
    if (parameter_count != 3) throw;
    return (get_this()->*m_mptr.m3)(p1, p2, p3);
  }
};

Unfortunately, in such an approach, it is possible to invoke any of the overloaded function operators, so for the method with two arguments, it is possible to invoke it with zero or one or any number of parameters. But luckily, the appropriate number of arguments can be stored in the parameter_count field and we can test its value before invoking the target method. From another side, events are never or almost never fired from outside its owners. The task for the programmer is to provide a method handler and not care about how and when this handler will be invoked. Now we can write the following code:

C++
void m()
{
  SomeClass sc;
  AnotherClass ac;

  MethodWithMaxThreParsHandler<int> with_0_parameters;
  MethodWithMaxThreParsHandler<char*, char> with_1_parameters;
  MethodWithMaxThreParsHandler<double, float, double> with_2_parameters;
  MethodWithMaxThreParsHandler<long, short, int, long> with_3_parameters;

  /*
   * Remember about adding pointer to
   * calling object with comma operator
   */
  with_0_parameters = &AnotherClass::m_with_0_par, &ac;
  with_1_parameters = &SomeClass::m_with_1_par, &sc;
  with_2_parameters = &AnotherClass::m_with_2_par, &ac;
  with_3_parameters = &SomeClass::m_with_3_par; // oops, object is missing

  int res0 = with_0_parameters();
  char* res1 = with_1_parameters('a');
  double res2 = with_2_parameters(0.3f, 3.14);
  long res3 = with_3_parameters(0, 1, 2); // will be boom from get_this
                                          // method at runtime

  IMGT dummy = NULL;

  res0 = with_0_parameters(0, dummy, dummy); // exception: delegate has 0 parameters
  res1 = with_1_parameters('b', NULL, dummy); // exception: delegate has 1 parameters
  res2 = with_2_parameters(0.1f); // exception: delegate has 2 parameters
  res3 = with_3_parameters(10, 11); // exception: delegate has 3 parameters
}

In most cases, it is the compiler that discovers incorrect casting error from types, e.g., both 0 and NULL are proper values and we will not have an exception till runtime.

Disclaimer

The software and the accompanying files are distributed "as is" and without any warranties whether expressed or implied. No responsibilities for possible damages or even functionality can be taken. The user must assume the entire risk of using this software.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer
Poland Poland
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
-- There are no messages in this forum --