A C++ delegate library that supports synchronous, asynchronous and remote function invocation on any C++03 or higher based system. The library easily invokes lambda, static or instance function executing in a separate thread, process or processor. All communication mechanisms supported such as message queues, pipes, serial, TCP, and UDP.
Introduction
Nothing seems to garner the interest of C++ programmers more than delegates. In other languages, the delegate is a first-class feature so developers can use these well-understood constructs. In C++, however, a delegate is not natively available. Yet that doesn’t stop us programmers from trying to emulate the ease with which a delegate stores and invokes any callable function.
Delegates normally support synchronous executions, that is, when invoked, the bound function is executed within the caller’s thread of control. On multi-threaded applications, it would be ideal to specify the target function and the thread it should execute on without imposing function signature limitations. The library does the grunt work of getting the delegate and all argument data onto the destination thread. The idea behind this article is to provide a C++ delegate library with a consistent API that is capable of synchronous and asynchronous invocations on any callable function.
The features of the delegate library are:
- Any Compiler – standard C++ code for any compiler without weird hacks
- Any Function – invoke any callable function: member, static, or free
- Any Argument Type – supports any argument type: value, reference, pointer, pointer to pointer
- Multiple Arguments – supports multiple function arguments
- Function-Like Template Arguments - new in 2022 library update
- Synchronous Invocation – call the bound function synchronously
- Asynchronous Invocation – call the bound function asynchronously on a client specified thread
- Blocking Asynchronous Invocation - invoke asynchronously using blocking or non-blocking delegates
- Smart Pointer Support - bind an instance function using a raw object pointer or
std::shared_ptr
- Lambda Support - bind and invoke lambda functions asynchronously using delegates.
- Automatic Heap Handling – automatically copy argument data to the heap for safe transport through a message queue
- Fixed Block Allocator – optionally divert heap allocation to fixed block memory pools
- Any OS – easy porting to any OS. Win32 and
std::thread
ports included - 32/64-bit - Support for 32 and 64-bit projects.
- CMake Build - CMake supports most toolchains including Windows and Linux.
- Unit Tests - extensive unit testing of the delegate library included
- No External Libraries – delegate does not rely upon external libraries
- Ease of Use – match the
FastDelegate
API as close as possible
The delegate implementation significantly eases multithreaded application development by executing the delegate function with all of the function arguments on the thread of control that you specify. The framework handles all of the low-level machinery to safely invoke any function signature on a target thread.
CMake is used to create the build files. CMake is free and open-source software. Windows, Linux and other toolchains are supported. See the CMakeLists.txt file for more information.
See GitHub for the latest source code:
See related GitHub projects:
2022 Library Updates
The C++ delegate library was updated with the following features:
- Function-like delegate syntax
AsyncInvoke()
to simplify asynchronous function invocation - C++11 or higher C++ compiler required
The old vs. new syntax comparison is below. The old syntax below uses standard template arguments. It also requires using the number or function arguments as part of the delegate type (e.g., DelegateFree1<>
is one function argument delegate).
DelegateFree1<int> delegateFree = MakeDelegate(&FreeFuncInt);
delegateFree(123);
DelegateMember1<TestClass, TestStruct*> delegateMember =
MakeDelegate(&testClass, &TestClass::MemberFunc);
delegateMember(&testStruct);
The new syntax uses function-like template arguments for improved readability:
DelegateFree<void (int)> delegateFree = MakeDelegate(&FreeFuncInt);
delegateFree(123);
DelegateMember<void (TestClass(TestStruct*))>
delegateMember = MakeDelegate(&testClass, &TestClass::MemberFunc);
delegateMember(&testStruct);
Prefer the 2022 updated version of the library.
The article uses the old syntax for examples. The explanations, however, are accurate.
Delegates Background
If you’re not familiar with a delegate, the concept is quite simple. A delegate can be thought of as a super function pointer. In C++, there's no pointer type capable of pointing to all the possible function variations: instance member, virtual, const, static, and free (global). A function pointer can’t point to instance member functions, and pointers to member functions have all sorts of limitations. However, delegate classes can, in a type-safe way, point to any function provided the function signature matches. In short, a delegate points to any function with a matching signature to support anonymous function invocation.
Probably the most famous C++ delegate implementation is the FastDelegate
by Doug Clugston. I’ve used this code with great success on many different projects. It’s easy to use and I’ve yet to find a compiler that it doesn't work on.
While the usage of FastDelegate
is seamless (and fast!), examination of the code reveals numerous hacks and “horrible” casts to make it work universally across different compilers. When I first studied the source, I almost didn’t use it on a project just on looks alone because of the complexity. However, it got me to thinking; say I didn’t care so much about speed. Was it even possible to design a C++ standards compliant delegate? If so, could it match the interface and usability of FastDelegate
?
In practice, while a delegate is useful, a multicast version significantly expands its utility. The ability to bind more than one function pointer and sequentially invoke all registrars’ makes for an effective publisher/subscriber mechanism. Publisher code exposes a delegate container and one or more anonymous subscribers register with the publisher for callback notifications.
The problem with callbacks on a multithreaded system, whether it be a delegate-based or function pointer based, is that the callback occurs synchronously. Care must be taken that a callback from another thread of control is not invoked on code that isn’t thread-safe. Multithreaded application development is hard. It's hard for the original designer; it's hard because engineers of various skill levels must maintain the code; it's hard because bugs manifest themselves in difficult ways. Ideally, an architectural solution helps to minimize errors and eases application development.
Some may question why std::function
wasn’t used as the basis for an asynchronous delegate. Originally, I started implementing a version using std::function
for targeting any callable function and it worked except for one key feature: equality. I soon discovered that you can’t compare std::function
for equality which is necessary to unregister from the container. There seemed to be no easy, generic way around this. And without a means to remove previously added callable functions, the design was kaput. All is not lost. The delegate hierarchy I ultimately created actually ended up being an advantage for the feature set I was trying to accomplish. Plus it was fun to create.
The article I wrote here on CodeProject entitled “Asynchronous Multicast Callbacks with Inter-Thread Messaging” provided an asynchronous multicast callback similar in concept to what is proposed here, but the callback signature was fixed and only one templatized function argument was supported. It also limited the callback function type to static member or free functions. Instance member functions were not supported. The advantage of accepting these limitations is that the AsycnCallback<>
implementation is much simpler and compact.
This C++ delegate implementation is full featured and allows calling any function, even instance member functions, with any arguments either synchronously or asynchronously. The delegate
library makes binding to and invoking any function a snap.
Using the Code
I’ll first present how to use the code, and then get into the implementation details.
The delegate library is comprised of delegates and delegate containers. A delegate is capable of binding to a single callable function. A multicast delegate container holds one or more delegates in a list to be invoked sequentially. A single cast delegate container holds at most one delegate.
The primary delegate classes are listed below, where X
is the number of arguments in the target function signature. For instance, if the target signature uses one argument such as in void (int)
, then the DelegateFree1<>
version is used. Similarly, if three arguments are used as in void (int, float, char)
, the DelgateFree3<>
is utilized.
DelegateFreeX<>
DelegateFreeAsyncX<>
DelegateFreeAsyncWaitX<>
DelegateMemberX<>
DelegateMemberAsyncX<>
DelegateMemberAsyncWaitX<>
DelegateMemberSpX<>
DelegateMemberSpAsyncX<>
DelegateRemoteSendX<>
DelegateFreeRemoteRecvX<>
DelegateMemberRemoteRecvX<>
DelegateFreeX<>
binds to a free or static member function. DelegateMemberX<>
binds to a class instance member function. DelegateMemberSpX<>
binds to a class instance member function using a std::shared_ptr
instead of a raw object pointer. All versions offer synchronous function invocation.
DelegateFreeAsyncX<>
, DelegateMemberAsyncX<>
and DelegateMemberSpAsyncX<>
operate in the same way as their synchronous counterparts; except these versions offer non-blocking asynchronous function execution on a specified thread of control.
DelegateFreeAsyncWaitX<>
and DelegateMemberAsyncWaitX<>
provides blocking asynchronous function execution on a target thread with a caller supplied maximum wait timeout.
DelegateRemoteSendX<>
, DelegateFreeRemoteRecvX<>
and DelegateMemberRemoteRecvX<>
are explained in the article, Remote Procedure Calls using C++ Delegates.
The three main delegate container classes are:
SinglecastDelegateX<>
MulticastDelegateX<>
MulticastDelegateSafeX<>
SinglecastDelegateX<>
is a delegate container accepting a single delegate. The advantage of the single cast version is that it is slightly smaller and allows a return type other than void
in the bound function.
MulticastDelegateX<>
is a delegate container implemented as a singly-linked list accepting multiple delegates. Only a delegate bound to a function with a void
return type may be added to a multicast delegate container.
MultcastDelegateSafeX<>
is a thread-safe container implemented as a singly-linked list accepting multiple delegates. Always use the thread-safe version if multiple threads access the container instance.
Each container stores the delegate by value. This means the delegate is copied internally into either heap or fixed block memory depending on the mode. The user is not required to manually create a delegate on the heap before insertion into the container. Typically, the overloaded template function MakeDelegate()
is used to create a delegate instance based upon the function arguments.
Synchronous Delegates
All delegates are created with the overloaded MakeDelegate()
template function. The compiler uses template argument deduction to select the correct MakeDelegate()
version eliminating the need to manually specify the template arguments. For example, here is a simple free function.
void FreeFuncInt(int value)
{
cout << "FreeCallback " << value << endl;
}
To bind the free function to a delegate, create a DelegateFree1<int>
instance using MakeDelegate()
. The DelegateFree
template argument is the int
function parameter. MakeDelegate()
returns a DelegateFree1<int>
object and the following line invokes the function FreeFuncInt
using the delegate.
DelegateFree1<int> delegateFree = MakeDelegate(&FreeFuncInt);
delegateFree(123);
A member function is bound to a delegate in the same way, only this time MakeDelegate()
uses two arguments: a class instance and a member function pointer. The two DelegateMember1
template arguments are the class name and the function argument.
DelegateMember1<TestClass, TestStruct*> delegateMember =
MakeDelegate(&testClass, &TestClass::MemberFunc);
delegateMember(&testStruct);
Rather than create a concrete free or member delegate, typically a delegate container is used to hold one or more delegates. A delegate container can hold any delegate type. For example, a multicast delegate container that binds to any function with a void (int)
function signature is shown below.
MulticastDelegate1<int> delegateA;
A single cast delegate is created in the same way.
SinglecastDelegate1<int> delegateB;
A function signature that returns a value, such as float (int)
, is defined by adding an additional template argument.
SinglecastDelegate1<int, float> delegateC;
A SinglecastDelegate<>
may bind to a function that returns a value whereas a multicast versions cannot. The reason is that when multiple callbacks are invoked, which callback function return value should be used? The correct answer is none, so multicast containers only accept delegates with function signatures using void
as the return type.
More function arguments mean using the MulticastDelegate2
or MulticastDelegate3
versions. Currently, the library supports up to five function arguments.
MulticastDelegate2<int, int> delegateD;
MulticastDelegate3<float, int, char> delegateE;
Of course, more than just built-in pass by value argument types are supported.
MulticastDelegate3<const MyClass&, MyStruct*, Data**> delegateF;
Creating a delegate instance and adding it to the multicast delegate container is accomplished with the overloaded MakeDelegate()
function and operator+=
. Binding a free function or static function only requires a single function pointer argument.
delegateA += MakeDelegate(&FreeFuncInt);
An instance member function can also be added to any delegate container. For member functions, the first argument to MakeDelegate()
is a pointer to the class instance. The second argument is a pointer to the member function.
delegateA += MakeDelegate(&testClass, &TestClass::MemberFunc);
Check for registered clients first, then invoke callbacks for all registered delegates. If multiple delegates are stored within MulticastDelegate1<int>
, each one is called sequentially.
if (delegateA)
delegateA(123);
Removing a delegate instance from the delegate container uses operator-=
.
delegateA -= MakeDelegate(&FreeFuncInt);
Alternatively, Clear()
is used to remove all delegates within the container.
delegateA.Clear();
A delegate is added to the single cast container using operator=
.
SinglecastDelegate1<int, int> delegateF;
delegateF = MakeDelegate(&FreeFuncIntRetInt);
Removal is with Clear()
or assigning 0
.
delegateF.Clear();
delegateF = 0;
Asynchronous Non-Blocking Delegates
Up until this point, the delegates have all been synchronous. The asynchronous features are layered on top of the synchronous delegate implementation. To use asynchronous delegates, a thread-safe delegate container safely accessible by multiple threads is required. Locks protect the class API against simultaneous access. The “Safe
” version is shown below.
MulticastDelegateSafe1<TestStruct*> delegateC;
A thread pointer as the last argument to MakeDelegate()
forces creation of an asynchronous delegate. In this case, adding a thread argument causes MakeDelegate()
to return a DelegateMemberAsync1<>
as opposed to DelegateMember1<>
.
delegateC += MakeDelegate(&testClass, &TestClass::MemberFunc, &workerThread1);
Invocation is the same as the synchronous version, yet this time the callback function TestClass::MemberFunc()
is called from workerThread1
.
if (delegateC)
delegateC(&testStruct);
Here is another example of an asynchronous delegate being invoked on workerThread1
with std::string
and int
arguments.
MulticastDelegateSafe2<const std::string&, int> delegateH;
delegateH += MakeDelegate(&testClass, &TestClass::MemberFuncStdString, &workerThread1);
delegateH("Hello world", 2016);
Usage of the library is consistent between synchronous and asynchronous delegates. The only difference is the addition of a thread pointer argument to MakeDelegate()
. Remember to always use the thread-safe MulticastDelegateSafeX<>
containers when using asynchronous delegates to callback across thread boundaries.
The default behavior of the delegate library when invoking non-blocking asynchronous delegates is that arguments not passed by value are copied into heap memory for safe transport to the destination thread. This means all arguments will be duplicated. If your data is something other than plain old data (POD) and can’t be bitwise copied, then be sure to implement an appropriate copy constructor to handle the copying yourself.
Actually, there is a way to defeat the copying and really pass a pointer without copying what it’s pointing at. However, the developer must ensure that (a) the pointed to data still exists when the target thread invokes the bound function and (b) the pointed to object is thread safe. This technique is described later in the article.
For more examples, see main.cpp and DelegateUnitTests.cpp within the attached source code.
Bind to std::shared_ptr
Binding to instance member function requires a pointer to an object. The delegate library supports binding with a raw pointer and a std::shared_ptr
smart pointer. Usage is what you’d expect; just use a std::shared_ptr
in place of the raw object pointer in the call to MakeDelegate()
. Depending on if a thread argument is passed to MakeDelegate()
or not, a DelegateMemberSpX<>
or DelegateMemberSpAsyncX<>
instance is returned.
std::shared_ptr<TestClass> spObject(new TestClass());
auto delegateMemberSp = MakeDelegate(spObject, &TestClass::MemberFuncStdString);
delegateMemberSp("Hello world using shared_ptr", 2016);
The included VC2008 can’t use std::shared_ptr
because the compiler doesn’t support the feature. Run the VS2015 project for working examples using std::shared_ptr
.
Caution Using Raw Object Pointers
Certain asynchronous delegate usage patterns can cause a callback invocation to occur on a deleted object. The problem is this: an object function is bound to a delegate and invoked asynchronously, but before the invocation occurs on the target thread, the target object is deleted. In other words, it is possible for an object bound to a delegate to be deleted before the target thread message queue has had a chance to invoke the callback. The following code exposes the issue.
TestClass* testClassHeap = new TestClass();
auto delegateMemberAsync =
MakeDelegate(testClassHeap, &TestClass::MemberFuncStdString, &workerThread1);
delegateMemberAsync("Function async invoked on deleted object. Bug!", 2016);
delegateMemberAsync.Clear();
delete testClassHeap;
The example above is contrived, but it does clearly show that nothing prevents an object being deleted while waiting for the asynchronous invocation to occur. In many embedded system architectures, the registrations might occur on singleton objects or objects that have a lifetime that spans the entire execution. In this way, the application’s usage pattern prevents callbacks into deleted objects. However, if objects pop into existence, temporarily subscribe to a delegate for callbacks, then get deleted later the possibility of a latent delegate stuck in a message queue could invoke a function on a deleted object.
Fortunately, C++ smart pointers are just the ticket to solve these complex object lifetime issues. A DelegateMemberSpAsyncX<>
delegate binds using a std::shared_ptr
instead of a raw object pointer. Now that the delegate has a shared pointer, the danger of the object being prematurely deleted is eliminated. The shared pointer will only delete the object pointed to once all references are no longer in use. In the code snippet below, all references to testClassSp
are removed by the client code yet the delegate’s copy placed into the queue prevents TestClass
deletion until after the asynchronous delegate callback occurs.
std::shared_ptr<TestClass> testClassSp(new TestClass());
auto delegateMemberSpAsync =
MakeDelegate(testClassSp, &TestClass::MemberFuncStdString, &workerThread1);
delegateMemberSpAsync
("Function async invoked using smart pointer. Bug solved!", 2016);
delegateMemberSpAsync.Clear();
testClassSp.reset();
Actually, this technique can be used to call an object function, and then the object automatically deletes after the callback occurs. Using the above example, create a shared pointer instance, bind a delegate, and invoke the delegate. Now testClassSp
can go out of scope and TestClass::MemberFuncStdString
will still be safely called on workerThread1
. The TestClass
instance will delete by way of std::shared_ptr<TestClass>
once the smart pointer reference count goes to 0
after the callback completes without any extra programmer involvement.
std::shared_ptr<TestClass> testClassSp(new TestClass());
auto delegateMemberSpAsync =
MakeDelegate(testClassSp, &TestClass::MemberFuncStdString, &workerThread1);
delegateMemberSpAsync("testClassSp deletes after delegate invokes", 2016);
Asynchronous Blocking Delegates
A blocking delegate waits until the target thread executes the bound delegate function. Unlike non-blocking delegates, the blocking versions do not copy argument data onto the heap. They also allow function return types other than void
whereas the non-blocking delegates only bind to functions returning void
. Since the function arguments are passed to the destination thread unmodified, the function executes just as you'd expect a synchronous version including incoming/outgoing pointers and references.
Stack arguments passed by pointer/reference need not be thread-safe. The reason is that the calling thread blocks waiting for the destination thread to complete. This means that the delegate implementation guarantees only one thread is able to access stack allocated argument data.
A blocking delegate must specify a timeout in milliseconds or WAIT_INFINITE
. Unlike a non-blocking asynchronous delegate, which is guaranteed to be invoked, if the timeout expires on a blocking delegate, the function is not invoked. Use IsSuccess()
to determine if the delegate succeeded or not.
Adding a timeout as the last argument to MakeDelegate()
causes a DelegateFreeAsyncWaitX<>
or DelegateMemberAsyncWaitX<>
instance to be returned depending on if a free or member function is being bound. A "Wait
" delegate is typically not added to a delegate container. The typical usage pattern is to create a delegate and function arguments on the stack, then invoke. The code fragment below creates a blocking delegate with the function signature int (std::string&)
. The function is called on workerThread1
. The function MemberFuncStdStringRetInt()
will update the outgoing string msg
and return an integer to the caller.
DelegateMemberAsyncWait1<TestClass, std::string&, int> delegateI =
MakeDelegate(&testClass, &TestClass::MemberFuncStdStringRetInt,
&workerThread1, WAIT_INFINITE);
std::string msg;
int year = delegateI(msg);
if (delegateI.IsSuccess())
cout << msg.c_str() << " " << year << endl;
Using the keyword auto
with delegates simplifies the syntax considerably.
auto delegateI =
MakeDelegate(&testClass, &TestClass::MemberFuncStdStringRetInt,
&workerThread1, WAIT_INFINITE);
std::string msg;
int year = delegateI(msg);
if (delegateI.IsSuccess())
cout << msg.c_str() << " " << year << endl;
Asynchronous Lambda Invocation
Delegates can invoke non-capturing lambda functions asynchronously. The example below calls LambdaFunc1
on workerThread1
.
auto LambdaFunc1 = +[](int i) -> int
{
cout << "Called LambdaFunc1 " << i << std::endl;
return ++i;
};
auto lambdaDelegate1 = MakeDelegate(LambdaFunc1, &workerThread1, WAIT_INFINITE);
int lambdaRetVal2 = lambdaDelegate1(123);
Delegates are callable and therefore may be passed to the standard library. The example below shows CountLambda
executed asynchronously on workerThread1
by std::count_if
.
std::vector<int> v{ 1, 2, 3, 4, 5, 6, 7, 8, 9 };
auto CountLambda = +[](int v) -> int
{
return v > 2 && v <= 6;
};
auto countLambdaDelegate = MakeDelegate(CountLambda, &workerThread1, WAIT_INFINITE);
const auto valAsyncResult = std::count_if(v.begin(), v.end(),
countLambdaDelegate);
cout << "Asynchronous lambda result: " << valAsyncResult << endl;
Delegate Library
The delegate library contains numerous classes. A single include DelegateLib.h provides access to all delegate library features. The defines within DelegateOpt.h set the library options. The library is wrapped within a DelegateLib
namespace. Included unit tests help ensure a robust implementation. The table below shows the delegate class hierarchy.
DelegateBase
Delegate0<>
DelegateFree0<>
DelegateFreeAsync0<>
DelegateFreeAsyncWaitBase0<>
DelegateFreeAsyncWait0<>
DelegateMember0<>
DelegateMemberAsync0<>
DelegateMemberAsyncWaitBase0<>
DelegateMemberAsyncWait0<>
DelegateMemberSp0<>
DelegateMemberSpAsync0<>
Delegate1<>
DelegateFree1<>
DelegateFreeAsync1<>
DelegateFreeAsyncWaitBase0<>
DelegateFreeAsyncWait0<>
DelegateMember1<>
DelegateMemberAsync1<>
DelegateMemberAsyncWaitBase1<>
DelegateMemberAsyncWait1<>
DelegateMemberSp1<>
DelegateMemberSpAsync1<>
etc...
Throughout the following discussion, I’ll be using the one parameter version of the delegates.
DelegateBase
is a non-template, abstract
base class common to all delegate instances. Comparison operators and a Clone()
method define the interface.
class DelegateBase {
public:
virtual ~DelegateBase() {}
virtual bool operator==(const DelegateBase& rhs) const = 0;
virtual bool operator!=(const DelegateBase& rhs) { return !(*this == rhs); }
virtual DelegateBase* Clone() const = 0;
};
Delegate1<>
provides a template class with templatized function arguments. The operator()
function allows invoking the delegate function with the correct function parameters. Covariant overloading of Clone()
provides a more specific return type.
The Clone()
function is required by the delegate container classes. The delegate container needs to make copies of the delegate for storage into the list. Since the delegate container only knows about abstract base Delegate1<>
instances, it must use the Clone()
function when creating a duplicate copy.
template <class Param1, class RetType=void>
class Delegate1 : public DelegateBase {
public:
virtual RetType operator()(Param1 p1) const = 0;
virtual Delegate1<Param1, RetType>* Clone() const = 0;
};
Efficiently storing instance member functions and free functions within the same class proves difficult. Instead, two classes were created for each type of bound function. DelegateMember1<>
handles instance member functions. DelegateFree1<>
handles free and static functions.
DelegateMember1<>
binds to an instance member function. The addition of a TClass
template argument is added above the Param1
and RetType
required of the inherited Delegate1<>
base class. TClass
is required for binding to an instance member function. The delegate containers, however, cannot know about TClass
. The container list may only store the most common ancestor of DelegateMember1<>
and DelegateFree1<>
, which happens to be the Delegate1<>
interface.
Clone()
creates a new instance of the class. Bind()
takes a class instance and a member function pointer. The function operator()
allows invoking the delegate function assigned with Bind()
.
template <class TClass, class Param1, class RetType=void>
class DelegateMember1 : public Delegate1<Param1, RetType> {
public:
typedef TClass* ObjectPtr;
typedef RetType (TClass::*MemberFunc)(Param1);
typedef RetType (TClass::*ConstMemberFunc)(Param1) const;
DelegateMember1(ObjectPtr object, MemberFunc func) { Bind(object, func); }
DelegateMember1(ObjectPtr object, ConstMemberFunc func) { Bind(object, func); }
DelegateMember1() : m_object(0), m_func(0) { }
void Bind(ObjectPtr object, MemberFunc func) {
m_object = object;
m_func = func; }
void Bind(ObjectPtr object, ConstMemberFunc func) {
m_object = object;
m_func = reinterpret_cast<MemberFunc>(func); }
virtual DelegateMember1* Clone() const { return new DelegateMember1(*this); }
virtual RetType operator()(Param1 p1) {
return (*m_object.*m_func)(p1); }
virtual bool operator==(const DelegateBase& rhs) const {
const DelegateMember1<TClass, Param1, RetType>* derivedRhs =
dynamic_cast<const DelegateMember1<TClass, Param1, RetType>*>(&rhs);
return derivedRhs &&
m_func == derivedRhs->m_func &&
m_object == derivedRhs->m_object; }
bool Empty() const { return !(m_object && m_func); }
void Clear() { m_object = 0; m_func = 0; }
explicit operator bool() const { return !Empty(); }
private:
ObjectPtr m_object; MemberFunc m_func; };
DelegateFree1<>
binds to a free or static member function. Notice it inherits from Delegate1<>
just like DelegateMember1<>
. Bind()
takes a function pointer and operator()
allows subsequent invocation of the bound function.
template <class Param1, class RetType=void>
class DelegateFree1 : public Delegate1<Param1, RetType> {
public:
typedef RetType (*FreeFunc)(Param1);
DelegateFree1(FreeFunc func) { Bind(func); }
DelegateFree1() : m_func(0) { }
void Bind(FreeFunc func) { m_func = func; }
virtual DelegateFree1* Clone() const { return new DelegateFree1(*this); }
virtual RetType operator()(Param1 p1) {
return (*m_func)(p1); }
virtual bool operator==(const DelegateBase& rhs) const {
const DelegateFree1<Param1, RetType>* derivedRhs =
dynamic_cast<const DelegateFree1<Param1, RetType>*>(&rhs);
return derivedRhs &&
m_func == derivedRhs->m_func; }
bool Empty() const { return !m_func; }
void Clear() { m_func = 0; }
explicit operator bool() const { return !Empty(); }
private:
FreeFunc m_func; };
DelegateMemberAsync1<>
is the non-blocking asynchronous version of the delegate allowing invocation on a client specified thread of control. The operator()
function doesn’t actually call the target function, but instead packages the delegate and all function arguments onto the heap into a DelegateMsg1<>
instance for sending through the message queue using DispatchDelegate()
.
template <class TClass, class Param1>
class DelegateMemberAsync1 : public DelegateMember1<TClass, Param1>,
public IDelegateInvoker {
public:
DelegateMemberAsync1(ObjectPtr object, MemberFunc func, DelegateThread* thread) {
Bind(object, func, thread); }
DelegateMemberAsync1(ObjectPtr object, ConstMemberFunc func,
DelegateThread* thread) {
Bind(object, func, thread); }
DelegateMemberAsync1() : m_thread(0) { }
void Bind(ObjectPtr object, MemberFunc func, DelegateThread* thread) {
m_thread = thread;
DelegateMember1<TClass, Param1>::Bind(object, func); }
void Bind(ObjectPtr object, ConstMemberFunc func, DelegateThread* thread) {
m_thread = thread;
DelegateMember1<TClass, Param1>::Bind(object, func); }
virtual DelegateMemberAsync1<TClass, Param1>* Clone() const {
return new DelegateMemberAsync1<TClass, Param1>(*this); }
virtual bool operator==(const DelegateBase& rhs) const {
const DelegateMemberAsync1<TClass, Param1>* derivedRhs =
dynamic_cast<const DelegateMemberAsync1<TClass, Param1>*>(&rhs);
return derivedRhs &&
m_thread == derivedRhs->m_thread &&
DelegateMember1<TClass, Param1>::operator == (rhs); }
virtual void operator()(Param1 p1) {
if (m_thread == 0)
DelegateMember1<TClass, Param1>::operator()(p1);
else
{
Param1 heapParam1 = DelegateParam<Param1>::New(p1);
DelegateMemberAsync1<TClass, Param1>* delegate = Clone();
DelegateMsg1<Param1>* msg = new DelegateMsg1<Param1>(delegate, heapParam1);
m_thread->DispatchDelegate(msg);
}
}
virtual void DelegateInvoke(DelegateMsgBase** msg) {
DelegateMsg1<Param1>* delegateMsg = static_cast<DelegateMsg1<Param1>*>(*msg);
Param1 param1 = delegateMsg->GetParam1();
DelegateMember1<TClass, Param1>::operator()(param1);
DelegateParam<Param1>::Delete(param1);
delete *msg;
*msg = 0;
delete this;
}
private:
DelegateThread* m_thread;
};
Arguments come in different styles: by value, by reference, pointer and pointer to pointer. For non-blocking delegates, anything other than pass by value needs to have the data pointed to created on the heap to ensure the data is valid on the destination thread. The key to being able to save each parameter into DelegateMsg1<>
is the DelegateParam<>
class as used within the operator()
function below.
virtual void operator()(Param1 p1) {
if (m_thread == 0)
DelegateMember1<TClass, Param1>::operator()(p1);
else
{
Param1 heapParam1 = DelegateParam<Param1>::New(p1);
DelegateMemberAsync1<TClass, Param1>* delegate = Clone();
DelegateMsg1<Param1>* msg = new DelegateMsg1<Param1>(delegate, heapParam1);
m_thread->DispatchDelegate(msg);
}
}
DelegateMemberSpAsync1<>
is an non-blocking asychronous delegate that binds to a std::shared_ptr
instead of a raw object pointer. The implementation is the same as the non-Sp version, except all locations of TClass*
are replaced with std::shared_ptr<TClass>
.
template <class TClass, class Param1, class RetType=void>
class DelegateMemberSp1 : public Delegate1<Param1, RetType> {
public:
typedef std::shared_ptr<TClass> ObjectPtr;
typedef RetType (TClass::*MemberFunc)(Param1);
typedef RetType (TClass::*ConstMemberFunc)(Param1) const;
DelegateMemberSp1(ObjectPtr object, MemberFunc func) { Bind(object, func); }
DelegateMemberSp1(ObjectPtr object, ConstMemberFunc func) { Bind(object, func); }
DelegateMemberSp1() : m_object(0), m_func(0) { }
etc...
DelegateMemberAsyncWait1<>
is a blocking asynchronous delegate that binds to a class instance member function. DelegateMemberAsyncWait1<>
has a template specialization for void
return types so that functions with and without return values are supported. A common base class DelegateMemberAsyncWaitBase1<>
are shared by the specializations. The two key functions are shown below. Notice that the implementation requires a semaphore to block the calling thread and a software lock to protect shared data.
virtual RetType operator()(Param1 p1) {
if (this->m_thread == 0)
return DelegateMemberAsyncWaitBase1<TClass, Param1, RetType>::operator()(p1);
else {
DelegateMemberAsyncWait1<TClass, Param1, RetType>* delegate = Clone();
delegate->m_refCnt = 2;
delegate->m_sema.Create();
delegate->m_sema.Reset();
DelegateMsg1<Param1>* msg = new DelegateMsg1<Param1>(delegate, p1);
this->m_thread->DispatchDelegate(msg);
if ((this->m_success = delegate->m_sema.Wait(this->m_timeout)))
m_retVal = delegate->m_retVal;
bool deleteData = false;
{
LockGuard lockGuard(&delegate->m_lock);
if (--delegate->m_refCnt == 0)
deleteData = true;
}
if (deleteData) {
delete msg;
delete delegate;
}
return m_retVal;
}
}
virtual void DelegateInvoke(DelegateMsgBase** msg) {
bool deleteData = false;
{
DelegateMsg1<Param1>* delegateMsg = static_cast<DelegateMsg1<Param1>*>(*msg);
Param1 param1 = delegateMsg->GetParam1();
LockGuard lockGuard(&this->m_lock);
if (this->m_refCnt == 2) {
m_retVal =
DelegateMemberAsyncWaitBase1
<TClass, Param1, RetType>::operator()(param1);
this->m_sema.Signal();
}
if (--this->m_refCnt == 0)
deleteData = true;
}
if (deleteData) {
delete *msg;
*msg = 0;
delete this;
}
}
Argument Heap Copy
Non-blocking asynchronous invocations means that all argument data must be copied into the heap for transport to the destination thread. The DelegateParam<>
class is used to new
/delete
arguments. Template specialization is used to define different versions of DelegateParam<>
based on the argument type: pass by value, reference, pointer, pointer to pointer. The snippet below shows how it’s used to make a copy of function argument p1
on the heap.
Param1 heapParam1 = DelegateParam<Param1>::New(p1);
The actual New()
function implementation called above depends on the Param1
argument type. Pass by value will call the template version shown below. It actually doesn’t create anything on the heap but instead just returns back the callers input value. The reason is that pass by value doesn’t need a heap copy as it already is a copy. Delete()
therefore does nothing as there is no data to delete.
template <typename Param>
class DelegateParam
{
public:
static Param New(Param param) { return param; }
static void Delete(Param param) { }
};
The DelegateParam<Param *>
template specialization below handles all pointer type arguments. Unlike pass by value, a pointer points at something. That something must be created on the heap so the destination thread has a full copy upon callback invocation. New()
creates the copy, and Delete()
deletes it.
template <typename Param>
class DelegateParam<Param *>
{
public:
static Param* New(Param* param) {
Param* newParam = new Param(*param);
return newParam;
}
static void Delete(Param* param) {
delete param;
}
};
Similarly, there are template specializations that handle references and pointers to pointers. This way, no matter the argument type, the delegate
library behaves in a consistent and correct way with no awareness or special effort on the user's part.
Bypassing Argument Heap Copy
Occasionally, you may not want the delegate
library to copy your pointer/reference arguments. Instead, you just want the destination thread to have a pointer to the original copy. Maybe the object is large or can’t be copied. Or maybe it’s a static
instance that is guaranteed to exist. Either way, here is how to really send a pointer without duplicating the object pointed to.
The trick is to define a DelegateParam<>
template specialization to handle your specific class/struct
. In the example below, the structure TestStructNoCopy
will not be copied by the delegate library. The New()
function just returns the original pointer, and Delete()
does nothing. Now, any TestStructNoCopy*
delegate
function arguments will use your New()
/Delete()
and not the library’s default implementation.
template <>
class DelegateParam<TestStructNoCopy *>
{
public:
static TestStructNoCopy* New(TestStructNoCopy* param) { return param; }
static void Delete(TestStructNoCopy* param) {}
};
Using this technique means that the pointer you’re passing must exist when the destination thread actually invokes the callback function. In addition, if multiple threads are accessing that instance, the code within the class needs to be thread-safe.
This method is not required on blocking delegates, as the arguments are not copied.
Array Argument Heap Copy
Array function arguments are adjusted to a pointer per the C standard. In short, any function parameter declared as T a[]
or T a[N]
is treated as though it were declared as T *a
. This means by default, the delegate library DelegateParam<Param *>
is called for array type parameters. Since the array size is not known, the DelegateParam<Param *>
will only copy the first array element which is certainly not what is expected or desired. For instance, the function below:
void ArrayFunc(char a[]) {}
Requires a delegate
argument char*
because the char a[]
was “adjusted” to char *a
.
MulticastDelegateSafe1<char*> delegateArrayFunc;
delegateArrayFunc += MakeDelegate(&ArrayFunc, &workerThread1);
There is no way to asynchronously pass a C-style array by value. The best that can be achieved is to pass the array by pointer using the previously described template specialization technique. The class below passes every char*
, char a[]
or char a[N]
as a char*
and the array pointer will be passed to the invoked function without attempting a copy. Remember, it is up to you to ensure the pointer remains valid on the destination thread.
template <>
class DelegateParam<char *>
{
public:
static char* New(char* param) { return param; }
static void Delete(char* param) {}
};
My recommendation is to avoid C-style arrays if possible when using asynchronous delegates to avoid confusion and mistakes.
Worker Thread (Win32)
After the operator()
function completes and the DelegateMsg1<>
is put into the message queue, eventually WorkerThread::Process()
will call DelegateInvoke()
on the destination thread. The Win32 thread loop code below is from WorkerThreadWin.cpp/h.
unsigned long WorkerThread::Process(void* parameter)
{
MSG msg;
BOOL bRet;
MMRESULT timerId = timeSetEvent(100, 10, &WorkerThread::TimerExpired,
reinterpret_cast<DWORD>(this), TIME_PERIODIC);
while ((bRet = GetMessage(&msg, NULL, WM_USER_BEGIN, WM_USER_END)) != 0)
{
switch (msg.message)
{
case WM_DISPATCH_DELEGATE:
{
ASSERT_TRUE(msg.wParam != NULL);
ThreadMsg* threadMsg = reinterpret_cast<ThreadMsg*>(msg.wParam);
DelegateMsgBase* delegateMsg = static_cast<DelegateMsgBase*>
(threadMsg->GetData());
delegateMsg->GetDelegateInvoker()->DelegateInvoke(&delegateMsg);
delete threadMsg;
break;
}
case WM_USER_TIMER:
Timer::ProcessTimers();
break;
case WM_EXIT_THREAD:
timeKillEvent(timerId);
return 0;
default:
ASSERT();
}
}
return 0;
}
Notice the thread loop is unlike most systems that have a huge switch
statement handling various incoming data messages, type casting void*
data, then calling a specific function. The framework supports all delegate invocations with a single WM_DISPATCH_DELEGATE
message. Once setup, the same small thread loop handles every delegate. New asynchronous delegates come and go as the system is designed, but the code in-between doesn't change.
This is a huge benefit as on many systems, getting data between threads takes a lot of manual steps. You constantly have to mess with each thread loop, create during sending, destroy data when receiving, and call various OS services and typecasts. Here, you do none of that. All the stuff in-between is neatly handled for users.
The DelegateMemberAsync1<>::DelegateInvoke()
function calls the target function and deletes the data that traveled through the message queue inside DelegateMsg1<>
. The delegate deletes all heap data and itself before returning.
virtual void DelegateInvoke(DelegateMsgBase** msg) const {
DelegateMsg1<Param1>* delegateMsg = static_cast<DelegateMsg1<Param1>*>(*msg);
Param1 param1 = delegateMsg->GetParam1();
DelegateMember1<TClass, Param1, RetType>::operator()(param1);
DelegateParam<Param1>::Delete(param1);
delete *msg;
*msg = 0;
delete this;
}
Worker Thread (std::thread)
Instead of the Win32 API, an alternate implementation using the std::thread
classes is included. Any C++11 compiler that supports std::thread
is able to build and use the delegate library. Within DelegateOpt.h, define USE_STD_THREADS
instead of USE_WIN32_THREADS
to use the WorkerThread
class contained within WorkerThreadStd.cpp/h. The LockGuard
and Semaphore
classes are also conditionally compiled to use the C++ Standard Library instead of the Win32 API. The std::thread
implemented thread loop is shown below:
void WorkerThread::Process()
{
m_timerExit = false;
std::thread timerThread(&WorkerThread::TimerThread, this);
while (1)
{
std::shared_ptr<ThreadMsg> msg;
{
std::unique_lock<std::mutex> lk(m_mutex);
while (m_queue.empty())
m_cv.wait(lk);
if (m_queue.empty())
continue;
msg = m_queue.front();
m_queue.pop();
}
switch (msg->GetId())
{
case MSG_DISPATCH_DELEGATE:
{
ASSERT_TRUE(msg->GetData() != NULL);
auto delegateMsg = static_cast<DelegateMsgBase*>(msg->GetData());
delegateMsg->GetDelegateInvoker()->DelegateInvoke(&delegateMsg);
break;
}
case MSG_TIMER:
Timer::ProcessTimers();
break;
case MSG_EXIT_THREAD:
{
m_timerExit = true;
timerThread.join();
return;
}
default:
ASSERT();
}
}
}
Delegate Invocation
A bound delegate function is invoked with the function operator()
. When invoking a delegate from a container, three function calls are required. One non-virtual operator()
within the delegate container, a second virtual
operator()
on the delegate, then finally the bound function is called.
For a multicast delegate, the container operator()
function iterates over the list calling each delegate’s operator()
. Notice, there is no return value when executing a delegate function within a multicast delegate container.
template<typename Param1>
class MulticastDelegate1 : public MulticastDelegateBase
{
public:
void operator()(Param1 p1) {
InvocationNode* node = GetInvocationHead();
while (node != 0) {
const Delegate1<Param1, void>* delegate =
static_cast<const Delegate1<Param1, void>*>(node->Delegate);
(*delegate)(p1); node = node->Next;
}
}
};
For a single cast delegate, the delegate container operator()
just calls the delegate operator()
. Notice that a return value is permitted using the single cast container.
RetType operator()(Param p1) {
return (*m_delegate)(p1); }
The containers don’t have awareness of the concrete delegate types. If the delegate stored within the container is the synchronous version, the bound function is called synchronously. When asynchronous delegates are invoked, the delegate and arguments are sent through a message queue to the destination thread.
When using a delegate without a container, the invocation is goes from the delegate virtual operator()
to the bound function. The snippet below shows invoking an instance member function.
virtual RetType operator()(Param1 p1) const {
return (*m_object.*m_func)(p1); }
Obtaining the utmost speed from the delegate wasn’t a priority especially when you involve a message queue; a few extra instructions aren’t likely to matter much. A standard compliant delegate with the ability to derive additional functionality trumped any such optimizations.
Delegate Containers
Delegate containers store one or more delegates. The delegate container hierarchy is shown below:
MulticastDelegateBase
MulticastDelegate0
MulticastDelegateSafe0
MulticastDelegate1<>
MulticastDelegateSafe1<>
etc...
SinglecastDelegate0<>
SinglecastDelegate1<>
etc...
The MulticastDelegateBase
provides a non-template base for storing non-template DelegateBase
instances within a list. The operator+=
add a delegate to the list and operator-=
removes it.
class MulticastDelegateBase
{
public:
MulticastDelegateBase() : m_invocationHead(0) {}
virtual ~MulticastDelegateBase() { Clear(); }
bool Empty() const { return !m_invocationHead; }
void Clear();
protected:
struct InvocationNode
{
InvocationNode() : Next(0), Delegate(0) { }
InvocationNode* Next;
DelegateBase* Delegate;
};
void operator+=(DelegateBase& delegate);
void operator-=(DelegateBase& delegate);
...
MulticastDelegate1<>
provides the function operator()
to sequentially invoke each delegate within the list. A simple cast is required to get the DelegateBase
typed back to a more specific Delegate1<>
instance.
template<typename Param1>
class MulticastDelegate1 : public MulticastDelegateBase
{
public:
MulticastDelegate1() { }
void operator()(Param1 p1) {
InvocationNode* node = GetInvocationHead();
while (node != 0) {
Delegate1<Param1>* delegate =
static_cast<Delegate1<Param1>*>(node->Delegate);
(*delegate)(p1); node = node->Next;
}
}
void operator+=(Delegate1<Param1>& delegate)
{ MulticastDelegateBase::operator+=(delegate); }
void operator-=(Delegate1<Param1>& delegate)
{ MulticastDelegateBase::operator-=(delegate); }
private:
MulticastDelegate1(const MulticastDelegate1&);
MulticastDelegate1& operator=(const MulticastDelegate1&);
};
MulticastDelegateSafe1<>
provides a thread-safe wrapper around the delegate API. Each function provides a lock guard to protect against simultaneous access. The Resource Acquisition is Initialization (RAII) technique is used for the locks.
template<typename Param1>
class MulticastDelegateSafe1 : public MulticastDelegate1<Param1>
{
public:
MulticastDelegateSafe1() { LockGuard::Create(&m_lock); }
~MulticastDelegateSafe1() { LockGuard::Destroy(&m_lock); }
void operator+=(Delegate1<Param1>& delegate) {
LockGuard lockGuard(&m_lock);
MulticastDelegate1<Param1>::operator +=(delegate);
}
void operator-=(Delegate1<Param1>& delegate) {
LockGuard lockGuard(&m_lock);
MulticastDelegate1<Param1>::operator -=(delegate);
}
void operator()(Param1 p1) {
LockGuard lockGuard(&m_lock);
MulticastDelegate1<Param1>::operator ()(p1);
}
bool Empty() {
LockGuard lockGuard(&m_lock);
return MulticastDelegate1<Param1>::Empty();
}
void Clear() {
LockGuard lockGuard(&m_lock);
return MulticastDelegate1<Param1>::Clear();
}
explicit operator bool() {
LockGuard lockGuard(&m_lock);
return MulticastDelegateBase::operator bool();
}
private:
MulticastDelegateSafe1(const MulticastDelegateSafe1&);
MulticastDelegateSafe1& operator=(const MulticastDelegateSafe1&);
LOCK m_lock;
};
SysData Example
A few real-world examples will demonstrate common delegate usage patterns. First, SysData
is a simple class showing how to expose an outgoing asynchronous interface. The class stores system data and provides asynchronous subscriber notifications when the mode changes. The class interface is shown below:
class SysData
{
public:
MulticastDelegateSafe1<const SystemModeChanged&> SystemModeChangedDelegate;
static SysData& GetInstance();
void SetSystemMode(SystemMode::Type systemMode);
private:
SysData();
~SysData();
SystemMode::Type m_systemMode;
LOCK m_lock;
};
The subscriber interface for receiving callbacks is SystemModeChangedDelegate
. Calling SetSystemMode()
saves the new mode into m_systemMode
and notifies all registered subscribers.
void SysData::SetSystemMode(SystemMode::Type systemMode)
{
LockGuard lockGuard(&m_lock);
SystemModeChanged callbackData;
callbackData.PreviousSystemMode = m_systemMode;
callbackData.CurrentSystemMode = systemMode;
m_systemMode = systemMode;
if (SystemModeChangedDelegate)
SystemModeChangedDelegate(callbackData);
}
SysDataClient Example
SysDataClient
is a delegate subscriber and registers for SysData::SystemModeChangedDelegate
notifications within the constructor.
SysDataClient() :
m_numberOfCallbacks(0)
{
SysData::GetInstance().SystemModeChangedDelegate +=
MakeDelegate(this, &SysDataClient::CallbackFunction, &workerThread1);
SysDataNoLock::GetInstance().SystemModeChangedDelegate +=
MakeDelegate(this, &SysDataClient::CallbackFunction, &workerThread1);
}
SysDataClient::CallbackFunction()
is now called on workerThread1
when the system mode changes.
void CallbackFunction(const SystemModeChanged& data)
{
m_numberOfCallbacks++;
cout << "CallbackFunction " << data.CurrentSystemMode << endl;
}
When SetSystemMode()
is called, anyone interested in the mode changes are notified synchronously or asynchronously depending on the delegate type registered.
SysData::GetInstance().SetSystemMode(SystemMode::STARTING);
SysData::GetInstance().SetSystemMode(SystemMode::NORMAL);
SysDataNoLock Example
SysDataNoLock
is an alternate implementation that uses a private
MulticastDelegateSafe1<>
for setting the system mode asynchronously and without locks.
class SysDataNoLock
{
public:
MulticastDelegateSafe1<const SystemModeChanged&> SystemModeChangedDelegate;
static SysDataNoLock& GetInstance();
void SetSystemMode(SystemMode::Type systemMode);
void SetSystemModeAsyncAPI(SystemMode::Type systemMode);
SystemMode::Type SetSystemModeAsyncWaitAPI(SystemMode::Type systemMode);
private:
SysDataNoLock();
~SysDataNoLock();
MulticastDelegateSafe1<SystemMode::Type> SetSystemModeDelegate;
void SetSystemModePrivate(SystemMode::Type);
SystemMode::Type m_systemMode;
};
The constructor registers SetSystemModePrivate()
with the private
SetSystemModeDelegate
.
SysDataNoLock::SysDataNoLock() :
m_systemMode(SystemMode::STARTING)
{
SetSystemModeDelegate +=
MakeDelegate(this, &SysDataNoLock::SetSystemModePrivate, &workerThread2);
workerThread2.CreateThread();
}
The SetSystemMode()
function below is an example of an asynchronous incoming interface. To the caller, it looks like a normal function, but under the hood, a private member call is invoked asynchronously using a delegate. In this case, invoking SetSystemModeDelegate
causes SetSystemModePrivate()
to be called on workerThread2
.
void SysDataNoLock::SetSystemMode(SystemMode::Type systemMode)
{
SetSystemModeDelegate(systemMode);
}
Since this private
function is always invoked asynchronously on workerThread2
, it doesn't require locks.
void SysDataNoLock::SetSystemModePrivate(SystemMode::Type systemMode)
{
SystemModeChanged callbackData;
callbackData.PreviousSystemMode = m_systemMode;
callbackData.CurrentSystemMode = systemMode;
m_systemMode = systemMode;
if (SystemModeChangedDelegate)
SystemModeChangedDelegate(callbackData);
}
SysDataNoLock Reinvoke Example
While creating a separate private
function to create an asynchronous API does work, with delegates, it's possible to just reinvoke the same exact function just on a different thread. Perform a simple check whether the caller is executing on the desired thread of control. If not, a temporary asynchronous delegate is created on the stack and then invoked. The delegate and all the caller’s original function arguments are duplicated on the heap and the function is reinvoked on workerThread2
. This is an elegant way to create asynchronous APIs with the absolute minimum of effort.
void SysDataNoLock::SetSystemModeAsyncAPI(SystemMode::Type systemMode)
{
if (workerThread2.GetThreadId() != WorkerThread::GetCurrentThreadId())
{
auto delegate =
MakeDelegate(this, &SysDataNoLock::SetSystemModeAsyncAPI, &workerThread2);
delegate(systemMode);
return;
}
SystemModeChanged callbackData;
callbackData.PreviousSystemMode = m_systemMode;
callbackData.CurrentSystemMode = systemMode;
m_systemMode = systemMode;
if (SystemModeChangedDelegate)
SystemModeChangedDelegate(callbackData);
}
SysDataNoLock Blocking Reinvoke Example
A blocking asynchronous API can be hidden inside a class member function. The function below sets the current mode on workerThread2
and returns the previous mode. A blocking delegate is created on the stack and invoked if the caller isn't executing on workerThread2
. To the caller, the function appears synchronous, but the delegate ensures that the call is executed on the proper thread before returning.
SystemMode::Type SysDataNoLock::SetSystemModeAsyncWaitAPI(SystemMode::Type systemMode)
{
if (workerThread2.GetThreadId() != WorkerThread::GetCurrentThreadId())
{
auto delegate =
MakeDelegate(this, &SysDataNoLock::SetSystemModeAsyncWaitAPI,
&workerThread2,
WAIT_INFINITE);
return delegate(systemMode);
}
SystemModeChanged callbackData;
callbackData.PreviousSystemMode = m_systemMode;
callbackData.CurrentSystemMode = systemMode;
m_systemMode = systemMode;
if (SystemModeChangedDelegate)
SystemModeChangedDelegate(callbackData);
return callbackData.PreviousSystemMode;
}
Timer Example
Once a delegate framework is in place, creating a timer callback service is trivial. Many systems need a way to generate a callback based on a timeout. Maybe it's a periodic timeout for some low speed polling or maybe an error timeout in case something doesn't occur within the expected time frame. Either way, the callback must occur on a specified thread of control. A SinglecastDelegate0<>
used inside a Timer
class solves this nicely.
class Timer
{
public:
SinglecastDelegate0<> Expired;
void Start(UINT32 timeout);
void Stop();
};
Users create an instance of the timer and register for the expiration. In this case, MyClass::MyCallback()
is called in 1000ms.
m_timer.Expired = MakeDelegate(&myClass, &MyClass::MyCallback, &myThread);
m_timer.Start(1000);
Heap vs. Fixed Block
The heap is used to create duplicate copies of the delegate and function arguments. When adding a delegate to a multicast list, it is cloned using operator new
. The asynchronous delegate support requires copying the delegate and all arguments for placement into a message queue. Normally, the memory comes from the heap. On many systems, this is not a problem. However, some system can’t use the heap in an uncontrolled fashion due to the possibility of a heap fragmentation memory fault. This occurs when the heap memory gets chopped up into small blocks over long executions such that a memory request fails.
A fixed block memory allocator is included within the source files. Just uncomment the USE_XALLOCATOR
define within DelegateOpt.h to enable using the fixed allocator. When enabled, all dynamic memory requests originating from the delegate
library are routed to the fixed block allocators. The xallocator
also has the advantage of faster execution than the heap thus limiting the speed impact of dynamic memory allocation.
The entire delegate
hierarchy is routed to fixed block usage with a single XALLOCATOR
macro inside DelegateBase
.
class DelegateBase {
#if USE_XALLOCATOR
XALLOCATOR
#endif
The delegate
library copies function arguments when necessary for asynchronous support. The memory requests are routed to the fixed block allocator by way of the DelegateParam<>
class. Notice that if USE_ALLOCATOR
is defined, the memory is obtained within New()
is from xmalloc()
and the placement new
syntax is used to construct the object within the fixed block region. Inside Delete()
, the destructor is called manually then xfree()
is used to return the fixed block memory.
template <typename Param>
class DelegateParam<Param *>
{
public:
static Param* New(Param* param) {
#if USE_XALLOCATOR
void* mem = xmalloc(sizeof(*param));
Param* newParam = new (mem) Param(*param);
#else
Param* newParam = new Param(*param);
#endif
return newParam;
}
static void Delete(Param* param) {
#if USE_XALLOCATOR
param->~Param();
xfree((void*)param);
#else
delete param;
#endif
}
};
See the article “Replace malloc/free with a Fast Fixed Block Memory Allocator” for more information.
Porting
The code is an easy port to any platform. There are only three OS services required: threads, a semaphore and a software lock. The code is separated into five directories.
- Delegate - core delegate library implementation files
- Port – thread-specific files
- Examples – sample code showing usage
- VS2008 – Visual Studio 2008 project files
- VS2015 – Visual Studio 2015 project files
The Eclipse project files are located at the project root (.cproject and .project). Use the File > Import... > General > Existing Projects into Workspace option to add the project to your workspace.
The library has a single abstract
class DelegateThread
with a single pure virtual
function that needs to be implemented on each target OS.
virtual void DispatchDelegate(DelegateMsgBase* msg) = 0;
On most projects, I wrap the underlying raw OS calls into a thread class to encapsulate and enforce the correct behavior. Here, I provide ThreadWin
class as a wrapper over the CreateThread()
Windows API.
Once you have a thread class, just inherit the DelegateThread
interface and implement the DispatchDelegate()
function. Using the Win32 API, a simple post to a message queue is all that is required:
void ThreadWin::DispatchDelegate(DelegateMsgBase* msg)
{
ThreadMsg* threadMsg = new ThreadMsg(WM_DISPATCH_DELEGATE, msg);
PostThreadMessage(WM_DISPATCH_DELEGATE, threadMsg);
}
The alternate implementation using the C++ Standard Library adds the message to a std::queue
protected by a mutex.
void WorkerThread::DispatchDelegate(DelegateLib::DelegateMsgBase* msg)
{
ASSERT_TRUE(m_thread);
std::shared_ptr<ThreadMsg> threadMsg(new ThreadMsg(MSG_DISPATCH_DELEGATE, msg));
std::unique_lock<std::mutex> lk(m_mutex);
m_queue.push(threadMsg);
m_cv.notify_one();
}
Software locks are handled by the LockGuard
class. This class can be updated with locks of your choice, or you can use a different mechanism. Locks are only used in a few places. The Semaphore
class wraps the Windows event objects or std::mutex
required by the blocking delegate implementation.
In short, the library supports Win32 and std::thread
models by defining USE_WIN32_THREADS
or USE_STD_THREADS
within DelegateOpt.h. If your C++11 or higher compiler supports std::thread
, then you're good to go. For other OSs, just provide an implementation for DelegateThread::DispatchDelegate()
, update the LockGuard
and Semaphore
classes, and put a small amount of code in your thread loop to call DelegateInvoke()
and the delegate
library can be deployed on any platform.
Summary
All delegates can be created with MakeDelegate()
. The function arguments determine the delegate type returned.
Synchronous delegates are created using one argument for free functions and two for instance member functions.
auto freeDelegate = MakeDelegate(&MyFreeFunc);
auto memberDelegate = MakeDelegate(&myClass, &MyClass::MyMemberFunc);
Adding the thread argument creates a non-blocking asynchronous delegate.
auto freeDelegate = MakeDelegate(&MyFreeFunc, &myThread);
auto memberDelegate = MakeDelegate(&myClass, &MyClass::MyMemberFunc, &myThread);
If using C++11, a std::shared_ptr
can replace a raw instance pointer on synchronous and non-blocking asynchronous member delegates.
std::shared_ptr<MyClass> myClass(new MyClass());
auto memberDelegate = MakeDelegate(myClass, &MyClass::MyMemberFunc, &myThread);
Adding a timeout argument creates a blocking asynchronous delegate.
auto freeDelegate = MakeDelegate(&MyFreeFunc, &myThread, WAIT_INFINITE);
auto memberDelegate = MakeDelegate(&myClass, &MyClass::MyMemberFunc, &myThread, 5000);
Delegates are added/removed from multicast containers using operator+=
and operator-=
. All containers accept all delegate types.
MulticastDelegate1<int> multicastContainer;
multicastContainer += MakeDelegate(&MyFreeFunc);
multicastContainer -= MakeDelegate(&MyFreeFunc);
Use the thread-safe multicast delegate container when using asynchronous delegates to allow multiple threads to safely add/remove from the container.
MulticastDelegateSafe1<int> multicastContainer;
multicastContainer += MakeDelegate(&MyFreeFunc, &myThread);
multicastContainer -= MakeDelegate(&MyFreeFunc, &myThread);
Single cast delegates are added and removed using operator=
.
SinglecastDelegate1<int> singlecastContainer;
singlecastContainer = MakeDelegate(&MyFreeFunc);
singlecastContainer = 0;
All delegates and delegate containers are invoked using operator()
.
if (myDelegate)
myDelegate(123);
Use IsSuccess()
on blocking delegates before using the return value or outgoing arguments.
if (myDelegate) {
int outInt = 0;
int retVal = myDelegate(&outInt);
if (myDelegate.IsSuccess()) {
cout << outInt << retVal;
}
}
I’ve documented four different asynchronous multicast callback implementations here on CodeProject. Each version has its own unique features and advantages. The sections below highlight the main differences between each solution. See the References section below for links to each article.
This asynchronous delegate implementation strives to ease inter-thread communication by invoking functions and passing data between threads using C++ delegates. Remote delegates extend the library to include inter-process and inter-processor communications. See the References section below for a the explanation of the remote procedure call using C++ delegates implementation.
Asynchronous Multicast Callbacks in C
- Implemented in C
- Callback function is a free or static member only
- One callback argument supported
- Callback argument must be a pointer type
- Callback argument data copied with
memcpy
- Type-safety provided by macros
- Static array holds registered subscriber callbacks
- Number of registered subscribers fixed at compile time
- Fixed block memory allocator in C
- Compact implementation
Asynchronous Multicast Callbacks with Inter-Thread Messaging
- Implemented in C++
- Callback function is a free or static member only
- One callback argument supported
- Callback argument must be a pointer type
- Callback argument data copied with copy constructor
- Type-safety provided by templates
- Minimal use of templates
- Dynamic list of registered subscriber callbacks
- Number of registered subscribers expands at runtime
- Fixed block memory allocator in C++
- Compact implementation
Asynchronous Multicast Delegates in C++
- Implemented in C++
- C++ delegate paradigm
- Any callback function type (member, static, free)
- Multiple callback arguments supported (up to 5)
- Callback argument any type (value, reference, pointer, pointer to pointer)
- Callback argument data copied with copy constructor
- Type-safety provided by templates
- Heavy use of templates
- Dynamic list of registered subscriber callbacks
- Number of registered subscribers expands at runtime
- Fixed block memory allocator in C++
- Larger implementation
Asynchronous Multicast Delegates in Modern C++
- Implemented in C++ (i.e., C++17)
- C++ delegate paradigm
- Function signature delegate arguments
- Any callback function type (member, static, free)
- Multiple callback arguments supported (N arguments supported)
- Callback argument any type (value, reference, pointer, pointer to pointer)
- Callback argument data copied with copy constructor
- Type-safety provided by templates
- Heavy use of templates
- Variadic templates
- Template metaprogramming
- Dynamic list of registered subscriber callbacks
- Number of registered subscribers expands at runtime
- Compact implementation (due to variadic templates)
Conclusion
I’ve done quite a bit of multithreaded application development over the years. Invoking a function on a destination thread with data has always been a hand-crafted, time consuming process. This library generalizes those constructs and encapsulates them into a user-friendly delegate library.
The article proposes a C++ multicast delegate implementation supporting synchronous and asynchronous function invocation. Non-blocking asynchronous delegates offer fire-and-forget invocation whereas the blocking versions allow waiting for a return value and outgoing reference arguments from the target thread. Multicast delegate containers expand the delegate’s usefulness by allowing multiple clients to register for callback notification. Multithreaded application development is simplified by letting the library handle the low-level threading details of invoking functions and moving data across thread boundaries. The inter-thread code is neatly hidden away within the library and users only interact with an easy to use delegate API.
History
- 13th December, 2016
- 14th December, 2016
- Corrected grammatical errors
- Fixed bugs in example code
- 16th December, 2016
- 19th December, 2016
- Added reinvoke example to article and attached source code
- 20th December, 2016
- Added support for
std::shared_ptr
to attached source code - Updated article to explain new feature
- 29th December, 2016
- Added blocking delegates
- Updated existing implementation with various improvements
- Updated article to explain new features
- Updated attached source code
- 1st January, 2017
- Updated delegate classes to build on GCC 5.4.0
- 7th January, 2017
- Added
std::thread
support to library - Fixed various bugs
- Added Eclipse project files for building source using GCC
- Updated article to explain new features
- 21st April, 2017
- Fixed bug in handling pointer to pointer asynchronous function arguments. Updated
DelegateParam<Param **>
class - Updated attached source code
- 18th January, 2019
- 28th January, 2020
- Fixed bug in DelegateMsg.h. Uploaded new source code
- 4th April, 2020
- Added remote delegate feature to this library. See References section
- Updated attached source code
- 2nd October, 2020
- Added
Timer
class - Updated Win32 and
std::thread
class implementations - Minor article updates. Added reference to new 'modern' delegate C++ implementation article
- Updated attached source code
- 14th October, 2022
- 2022 library updates
- Updated attached source code
- 20th September, 2022
- Updates to simplify 2022 library implementation
- Updated attached source code
- 12th December, 2022
- Using delegate with lambda updates
- New source code attached.