Delegate in C++ is not a new concept. There are a lot of implementations on Code Project and the Internet in general. In my opinion, the most comprehensive article is by Don Clugston in which Don shows all of the difficulties that people are facing with pointer-to-method. On the other hand, Sergey Ryazanov introduces a very simple solution that utilizes the "non-type template parameter" feature of modern C++ language. With Boost users, Boost.Function should be the most famous one. Here is a comparison of these implementations.

Use dynamic memory to store information when binding to method and bound object.

Make use of a non-type template parameter. Very easy to understand.

Due to the fact that each compiler stores pointer-to-method in different ways, Don defines a uniform format for his delegate. Then depending on each compiler, he converts from compiler's format to his own format.

Memory footprint

Not efficient due to expensive heap memory allocation.

Efficient

Efficient

Standard compliant

Yes

Yes

No. Actually, it's a hack.

Portable

Yes

No. Not all C++ compilers support non-type template argument.

Yes. But just at this moment for all C++ compilers that he knew and not sure in future.

The answer is YES if you feel comfortable in C++ template programming. Otherwise, the answer may be NO.

Yes. It is understandable even if readers are just beginners in template programming.

No. You are required to have a deep knowledge of pointers and compilers to be able to understand his code. Don has provided a comprehensive tutorial in his article.

Note: Some people did a comparison of the speeds of invocation between these listed Delegates. However, in my opinion, the difference of several hundreds of milliseconds for 10,000,000 Delegate invocations is not very significant.

An ideal delegate should be standard-compliant and portable (Boost), have efficient memory usage (Sergey & Don), nice syntax (Boost & Don), comparable (Boost & Don) and KISS (Sergey). Could we improve one of them to be the ideal one, or is there a new one? Yes, I'm going to show you a new one.

If you compile and run the demo with VC++ using default project settings, the output will be:

481216

The output of the above example will not be identical when compiling with other C++ compilers. Refer to the section entitled "Implementations of Member Function Pointers" in Don's article for more information. Obviously, the pointer-to-method of an "unknown" class is always the biggest one in size, excepting compilers. From now on in this article, we distinguish a C++ class into 2 categories:

As you have just seen, we force the compiler to convert the pointer-to-method of a known class to a pointer-to-method of an unknown class. In other words, we convert a pointer-to-method from its smallest format to its largest format. In this way, we have a unified format for all kinds of pointers to functions/methods. As a result, comparison between Delegate instances is easy. It's simply a call to the standard memcmp C function.

First, the "switch... case" statement makes this implementation run a little slower than others.

Second, if we want to extend the delegate to have more features -- i.e. support for reference counter mechanisms like smart pointer or COM interface -- we need more storage for that information.

Polymorphism might be an answer. However, the "one-size-fits-all" characteristic of Delegate is the main reason for its existence. Due to that fact, all methods or operators of Delegate class templates must be made as NON-virtual. At this point, many will remember the so-called "Strategy Design Pattern." Yes, it is also my choice. However, there are still things that need to be considered:

Using the "Strategy Design Pattern" introduces a little overhead when invoking Delegate: the user application passes parameters to the delegate; the delegate passes parameters to its strategy and the strategy again passes parameters to the real method or function. However, if arguments are all simple types such as char, long, int, pointer, and reference then the compiler will automatically generate optimizing code that removes such overhead.

Who should hold data: Strategy or Delegate? Data here means the pointer-to-object (_never_exist_class* m_p) and pointer-to-address of the method or function (greatest_pointer_type m_fn). If Delegate holds data, it must pass data to Strategy. Such operations suppress the compiler from optimizing the code. If Strategy holds data, the Strategy object must be created dynamically. This involves expensive memory allocations (new, delete operation).

The two problems are resolved if we apply the Strategy Design Pattern with a little modification:

To allow the compiler to optimize the code, we put data into Strategy. Note: Putting data into Strategy causes it to look like the Bridge Design pattern, but this is not important.

To avoid dynamic memory allocation, we will embed the whole Strategy object into the Delegate object instead of keeping a pointer to it as usual

When binding an object and its method to a delegate instance, the delegate normally just keeps the address of the object and the address of the method for later invocation. There are 2 possible problems that may cause our application to crash during runtime:

What happens if the method is inside a DLL but that DLL is already unloaded out of process' space? We have no way to deal with such situations so we simply ignore this problem.

What happens if the object is deleted somehow, somewhere due to a developer's mistake? The simple answer is: Developers must take care by themselves to avoid this mistake. However, manual object management is always tedious, error-proven and slows down developers' performance. So I have tried to find a simple but good enough mechanism for this purpose. The following is such a one:

Boost introduces the Clonable & Clone Allocator concepts. Although it's not flexible for many purposes, its simplicity will not make this Delegate library complicated. For that reason, this library makes use of the concepts in Boost and exposes the following handy Clone Allocator classes.

Class <a href="http://www.boost.org/libs/ptr_container/doc/reference.html#class-view-clone-allocator">view_clone_allocator</a> is an allocator that does nothing. It's identical to the one with the same name in Boost. When creating a delegate instance, if we don't give a specified allocator, this one will be used by default.

Class <a href="http://www.boost.org/libs/ptr_container/doc/reference.html#class-heap-clone-allocator">heap_clone_allocator</a> which is also identical to the one with the same name in Boost. It uses dynamic memory allocation and copy constructor to clone the bound object.

Class com_autoref_clone_allocator is provided to support COM interface. It should also work for any class objects that implement the two methods AddRef & Release in their right meaning.

One rule should be remembered when doing assignments between 2 Delegate instances: the target Delegate instance would use the clone allocator of the source to clone object. Logic of assignment is as follows:

At first, the target delegate (left side) frees its object using its current clone allocator.

All information of the source (right side) would be copied to the target including clone allocator. Actually, this is a simple bit-wise copy.

The target would clone the new object that it's holding using the new clone allocator.

And so on for later assignments between delegate instances.

Note: Actually, in real implementation I already considered and eliminated the problem of self-assignment, in which source and target are identical.

In some cases, we want to bind an already-cloned object to a delegate instance. If so, we want the delegate to free the object automatically, but not clone it again. In order to achieve this purpose, when binding an object and its method to a delegate, we have to provide 2 additional pieces of information: the clone allocator class is the first one; the second is a Boolean value to tell whether the delegate should clone the object or not.

With non-relaxed delegate libraries, template parameter types passed over to the delegate are very strictly checked. For example, if we assign a function with prototype int (*)(long) to a delegate with prototype long (*)(int), the compiler will raise errors saying that the assignment is not allowed since int and long are of different types. Actually, such conversion is safe because it meets the following three conditions:

The number of arguments matches.

Each matching argument can be implicitly converted from the delegate's argument to the target function's argument by the compiler.

The return-type can be implicitly converted from the target function's return-type to the delegate's return-type by the compiler. The void return-type is a special case: we can bind a delegate that returns void to any method or function that satisfies the two above conditions. This is the same as when we call a function/method, but don't care about its returned value.

If we pass simple data types (char, int, pointer…) to any one of the listed Delegate's implementations, the compiler will generate optimizing code. So the speeds of invocation are not much different between them. However, if we pass complex data types that involve a constructor and destructor, the 2nd syntax is faster than any other.

This article shows a way of converting various formats of pointer-to-method/free-function into a uniform format; the conversion is achieved in a standard compliant way. As a result, comparison between delegate instances is just easy.

The Strategy Design Pattern with a little modification makes this Delegate fast and extensible.

It is a kind of KISS method: people with a limited background in C++ template programming can understand the source code.

What we discussed in this article is about the Singlecast Delegate. There is another kind, the so-called Multicast Delegate, which is internally just a collection of other delegates. When we make an invocation on Multicast, all Singlecast delegates in the collection are also invoked one-by-one. Mainly, Multicast is used to implement the Observer Design Pattern.

Share

About the Author

Quynh Nguyen is a Vietnamese who has worked for 7 years in Software Outsourcing area. Currently, he works for Global Cybersoft (Vietnam) Ltd. as a Project Manager in Factory Automation division.

In the first day learning C language in university, he had soon switched to Assembly language because he was not able to understand why people cannot get address of a constant as with a variable. With that stupid starting, he had spent a lot of his time with Assembly language during the time he was in university.

Now he is interesting in Software Development Process, Software Architecture and Design Pattern… He especially indulges in highly concurrent software.

Great work, finally a C++ delegate implementation that doesn't make me want to puke.

It is much more difficult to create something simple and readable than it is to create something complicated and illegible. Many developers who rely heavily on C++ templates and metaprogramming seem to forget this.

And many people on the C++ standards seem to think that extending the syntax of the language is more important than reducing the number of undefined or implementation dependent features which are holding it back.

After all what is so wrong with a language defining its own in memory representations of function pointers or having well defined orders of evalution in all types of expressions ?

For far too long the optimisation camp has stagnated the process. I think its time people evaluated the benefits of productivity over flexibility. C++ is more productive than C becuase it has features like dynamic binding, multiple inheritance, and exception handling. All of these features come at a performance cost, why cant we just accept that things which improve productivity are worth sacrificing performance for ?

So i still wish someone would have the sense to just "clean up" the mess and add a well defined delegate to the C++ langauge itself, but the next best thing is surely this library.

void (T::*)() can be casted to void (_another_never_exist_class_::*)() using static_cast

You are right. void (T::*)() can be casted to void (_another_never_exist_class_::*)() implicitlly by compiler, don't need static_cast or reinterpret_cast. However, m_fn is just a place holder (block of memory that can hold any kind of pointer).

Don't like other delegate implementations, this library supports Object Life-time Management. Due to that fact, it may introduce a strange behavior when implementing operator += and -=. Let see following example:

After statement "md += d", "md" will not contain "d" but a clone of "d" (see clone allocator[^] for more information). It's the reason why "md -= d" may be failed after which. I think a 'nice' solution for this problem cannot come soon as we expected

Just one small correction; your observation on Boost.Function using dynamic storage was correct until the recently released version 1.34. In this version, however (form the Boost homepage):

Boost.Function now implements a small buffer optimization, which can drastically improve the performance when copying or constructing Boost.Function objects storing small function objects. For instance, bind(&X:foo, &x, _1, _2) requires no heap allocation when placed into a Boost.Function object

I'm glad to see that you include such factors as 'syntax', and 'KISS' in your criteria, rather then just focusing on speed.

Also, though portability is important, it is good to see that you are only trying to please the compliant compilers (eg. => VC7.1 and GCC). Libraries made to work on all compilers often have code that is almost un-readable.

On the other hand, I am using Don's fastdelegate library which is focussed on speed and is hard to read. I choose it because, in my case, delegate speed is important and Don's solution is much faster then the alternatives. It would be nice if the compiler behavior that Don has identified could be standardized.

There have been a lot of articles, and a lot of discussion on this topic in the last few years. Hopefully all this attention will lead to standardization in the next version of c++.