A C++ standards compliant delegate library capable of targeting any callable function synchronously or asynchronously.
- Asynchronous Multicast Delegates in Modern C++
- Table of Contents
- Preface
- Introduction
- Delegates Background
- Quick Start
- Project Build
- Using the Code
- Delegate Library
- Delegate Containers
- Examples
- Testing
- Summary
- Which Callback Implementation?
- References
- Conclusion
Originally published on CodeProject at: Asynchronous Multicast Delegates in Modern C++
The repositories below utilize the delegate library in different multithreaded applications.
- Asynchronous State Machine Design in C++ - an asynchronous C++ state machine implemented using an asynchronous delegate library.
- Integration Test Framework using Google Test and Delegates - a multi-threaded C++ software integration test framework using Google Test and Delegate libraries.
- Asynchronous SQLite API using C++ Delegates - an asynchronous SQLite wrapper implemented using an asynchronous delegate library.
Asynchronous function invocation allows for easy movement of data between threads. The table below summarizes the various asynchronous function invocation implementations available in C and C++.
Repository | Language | Key Delegate Features | Notes |
---|---|---|---|
AsyncMulticastDelegateModern | C++17 | * Function-like template syntax * Any delegate target function type (member, static, free, lambda) * N target function arguments * N delegate subscribers * Variadic templates * Template metaprogramming |
* Most generic implementation * Lowest lines of source code * Slowest of all implementations * Optional fixed block allocator support * No remote delegate support * Complex metaprogramming |
AsyncMulticastDelegateCpp17 | C++17 | * Function-like template syntax * Any delegate target function type (member, static, free, lambda) * 5 target function arguments * N delegate subscribers * Optional fixed block allocator * Variadic templates |
* Selective compile using constexpr * Avoids complex metaprogramming * Faster than AsyncMulticastDelegateModern * No remote delegate support |
AsyncMulticastDelegateCpp11 | C++11 | * Function-like template syntax * Any delegate target function type (member, static, free, lambda) * 5 target function arguments * N delegate subscribers * Optional fixed block allocator |
* High lines of source code * Highly repetitive source code |
AsyncMulticastDelegate | C++03 | * Traditional template syntax * Any delegate target function type (member, static, free) * 5 target function arguments * N delegate subscribers * Optional fixed block allocator |
* High lines of source code * Highly repetitive source code |
AsyncCallback | C++ | * Traditional template syntax * Delegate target function type (static, free) * 1 target function argument * N delegate subscribers |
* Low lines of source code * Most compact C++ implementation * Any C++ compiler |
C_AsyncCallback | C | * Macros provide type-safety * Delegate target function type (static, free) * 1 target function argument * Fixed delegate subscribers (set at compile time) * Optional fixed block allocator |
* Low lines of source code * Very compact implementation * Any C compiler |
This article documents a modern C++ implementation of asynchronous delegates. The library implements anonymous synchronous and asynchronous function callbacks. The target function is invoked with all arguments on the registrar 's desired thread of control.
The previous article I wrote entitled "Asynchronous Multicast Delegates in C++" built under C++03. This "modern" version uses C++17 features. Variadic templates and template metaprogramming improve library usability and significantly reduces the source code line count. While the basic idea between the articles is similar, this new version is a complete rewrite.
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 modern C++ delegate library are:
- Any Compiler – standard C++17 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 N number of function arguments for the bound function
- 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
- Any OS – easy porting to any OS. C++11
std::thread
port included - 32/64-bit - Support for 32 and 64-bit projects.
- Dynamic Storage Allocation - Optional fixed block memory allocator.
- 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 – function signature template arguments (e.g.,
MulticastDelegate<void(TestStruct*)>
)
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 build are included for easy experimentation on Windows, Linux and other platforms.
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.
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.
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.
A simple publish/subscribe asynchronous delegate example.
Typically a delegate is inserted into a delegate container. AlarmCd
is a delegate container.
MulticastDelegateSafe
- the delegate container type.void(int, const string&)
- the function signature accepted by the delegate container. Any function matching can be inserted, such as a class member, static or lambda function.AlarmCb
- the delegate container name.
Invoke delegate container to notify subscribers.
MulticastDelegateSafe<void(int, const string&)> AlarmCb;
void NotifyAlarmSubscribers(int alarmId, const string& note)
{
// Invoke delegate to generate callback(s) to subscribers
AlarmCb(alarmId, note);
}
Typically a subscriber registers with a delegate container instance to receive callbacks, either synchronously or asynchronously.
Figure 2: Insert into AlarmCb Delegate ContainerAlarmCb
- the publisher delegate container instance.+=
- add a function target to the container.MakeDelegate
- creates a delegate instance.&alarmSub
- the subscriber object pointer.&AlarmSub::MemberAlarmCb
- the subscriber callback member function.workerThread1
- the thread the callback will be invoked on. Adding a thread argument changes the callback type from synchronous to asynchronous.
Create a function conforming to the delegate signature. Insert a callable functions into the delegate container.
class AlarmSub
{
void AlarmSub()
{
// Register to receive callbacks on workerThread1
AlarmCb += MakeDelegate(this, &AlarmSub::HandleAlarmCb, workerThread1);
}
void ~AlarmSub()
{
// Unregister from callbacks
AlarmCb -= MakeDelegate(this, &AlarmSub::HandleAlarmCb, workerThread1);
}
void HandleAlarmCb(int alarmId, const string& note)
{
// Handle callback here. Called on workerThread1 context.
}
}
This is a simple example. Many other usage patterns exist including asynchronous API's, blocking delegates with a timeout, and more.
CMake is used to create the build files. CMake is free and open-source software. Windows, Linux and other toolchains are supported. Example CMake console commands executed inside the project root directory:
cmake -G "Visual Studio 17 2022" -A Win32 -B ../AsyncMulticastDelegateModernBuild -S .
cmake -G "Visual Studio 17 2022" -A x64 -B ../AsyncMulticastDelegateModernBuild -S .
cmake -G "Visual Studio 17 2022" -A x64 -B ../AsyncMulticastDelegateModernBuild -S . -DENABLE_UNIT_TESTS=ON
cmake -G "Visual Studio 17 2022" -A x64 -B ../AsyncMulticastDelegateModernBuild -S . -DENABLE_ALLOCATOR=ON
After executed, open the Visual Studio project from within the AsyncMulticastDelegateModernBuild
directory.
cmake -G "Unix Makefiles" -B ../AsyncMulticastDelegateModernBuild -S .
cmake -G "Unix Makefiles" -B ../AsyncMulticastDelegateModernBuild -S . -DENABLE_UNIT_TESTS=ON
cmake -G "Unix Makefiles" -B ../AsyncMulticastDelegateModernBuild -S . -DENABLE_ALLOCATOR=ON
After executed, build the software from within the AsyncMulticastDelegateModernBuild directory using the command make
. Run the console app using ./DelegateApp
.
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:
DelegateFree<>
DelegateFreeAsync<>
DelegateFreeAsyncWait<>
DelegateMember<>
DelegateMemberAsync<>
DelegateMemberAsyncWait<>
DelegateMemberSp<>
DelegateMemberSpAsync<>
DelegateFree<>
binds to a free or static member function. DelegateMember<>
binds to a class instance member function. DelegateMemberSp<>
binds to a class instance member function using a std::shared_ptr
instead of a raw object pointer. All versions offer synchronous function invocation.
DelegateFreeAsync<>
, DelegateMemberAsync<>
and DelegateMemberSpAsync<>
operate in the same way as their synchronous counterparts; except these versions offer non-blocking asynchronous function execution on a specified thread of control.
DelegateFreeAsyncWait<>
and DelegateMemberAsyncWait<>
provides blocking asynchronous function execution on a target thread with a caller supplied maximum wait timeout. The destination thread will not invoke the target function if the timeout expires.
The three main delegate container classes are:
SinglecastDelegate<>
MulticastDelegate<>
MulticastDelegateSafe<>
SinglecastDelegate<>
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 value.
MulticastDelegate<>
is a delegate container accepting multiple delegates.
MultcastDelegateSafe<>
is a thread-safe container 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 the heap. 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.
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 DelegateFree
instance using MakeDelegate()
. The DelegateFree
template argument is the complete function's signature: void(int)
. MakeDelegate()
returns a DelegateFree
object and the following line invokes the function FreeFuncInt
using the delegate.
// Create a delegate bound to a free function then invoke
DelegateFree<void(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 DelegateMember
template arguments are the class name (i.e., TestClass
) and the bound function signature (i.e. void(TestStruct*)
).
// Create a delegate bound to a member function then invoke
DelegateMember<TestClass, void(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:
MulticastDelegate<void(int)> delegateA;
A single cast delegate is created in the same way:
SinglecastDelegate<void(int)> delegateB;
A function signature that returns a value is also possible. The delegate container accepts functions with one float
argument and returns an int
.
SinglecastDelegate<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.
MulticastDelegate
containers bind to one or more functions.
MulticastDelegate<void(int, int)> delegateD;
MulticastDelegate<void(float, int, char)> delegateE;
Of course, more than just built-in pass by value argument types are supported.
MulticastDelegate<void(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, and then invoke callbacks for all registered delegates. If multiple delegates are stored within MulticastDelegate
, each one is called sequentially.
// Invoke the delegate target functions
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=
.
SinglecastDelegate<int(int)> delegateF;
delegateF = MakeDelegate(&FreeFuncIntRetInt);
Removal is with Clear()
or assigning 0
.
delegateF.Clear();
delegateF = 0;
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.
MulticastDelegateSafe<void(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 DelegateMemberAsync<>
as opposed to DelegateMember<>
.
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.
// Create delegate with std::string and int arguments then asynchronously
// invoke on a member function
MulticastDelegateSafe<void(const std::string&, int)> delegateH;
delegateH += MakeDelegate(&testClass, &TestClass::MemberFuncStdString, workerThread1);
delegateH("Hello world", 2020);
Usage of the library is consistent between synchronous and asynchronous delegates. The only difference is the addition of a thread pointer argument to MakeDelegate()
. Always remember to use the thread-safe MulticastDelegateSafe<>
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 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.
For more examples, see main.cpp and DelegateUnitTests.cpp within the attached source code.
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 DelegateMemberSp<>
or DelegateMemberSpAsync<>
instance is returned.
// Create a shared_ptr, create a delegate, then synchronously invoke delegate function
std::shared_ptr<TestClass> spObject(new TestClass());
auto delegateMemberSp = MakeDelegate(spObject, &TestClass::MemberFuncStdString);
delegateMemberSp("Hello world using shared_ptr", 2020);
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:
// Example of a bug where the testClassHeap is deleted before the asychronous delegate
// is invoked on the workerThread1. In other words, by the time workerThread1 calls
// the bound delegate function the testClassHeap instance is deleted and no longer valid.
TestClass* testClassHeap = new TestClass();
auto delegateMemberAsync =
MakeDelegate(testClassHeap, &TestClass::MemberFuncStdString, workerThread1);
delegateMemberAsync("Function async invoked on deleted object. Bug!", 2020);
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 DelegateMemberSpAsync<>
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.
// Example of the smart pointer function version of the delegate. The testClassSp instance
// is only deleted after workerThread1 invokes the callback function thus solving the bug.
std::shared_ptr<TestClass> testClassSp(new TestClass());
auto delegateMemberSpAsync = MakeDelegate
(testClassSp, &TestClass::MemberFuncStdString, workerThread1);
delegateMemberSpAsync("Function async invoked using smart pointer. Bug solved!", 2020);
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
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", 2020);
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 DelegateFreeAsyncWait<>
or DelegateMemberAsyncWait<>
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.
// Create a asynchronous blocking delegate and invoke. This thread will block until the
// msg and year stack values are set by MemberFuncStdStringRetInt on workerThread1.
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;
}
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;
};
// Asynchronously invoke lambda on workerThread1 and wait for the return value
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;
The delegate library contains numerous classes. A single include DelegateLib.h provides access to all delegate library features. 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
Delegate<>
DelegateFree<>
DelegateFreeAsync<>
DelegateFreeAsyncWait<>
DelegateMember<>
DelegateMemberAsync<>
DelegateMemberAsyncWait<>
DelegateMemberSp<>
DelegateMemberSpAsync<>
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() {}
/// Derived class must implement operator== to compare objects.
virtual bool operator==(const DelegateBase& rhs) const = 0;
virtual bool operator!=(const DelegateBase& rhs) { return !(*this == rhs); }
/// Use Clone to provide a deep copy using a base pointer. Covariant
/// overloading is used so that a Clone() method return type is a
/// more specific type in the derived class implementations.
/// @return A dynamic copy of this instance created with operator new.
/// @post The caller is responsible for deleting the clone instance.
virtual DelegateBase* Clone() const = 0;
};
Delegate<>
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 Delegate<>
instances, it must use the Clone()
function when creating a duplicate copy.
template <class R>
struct Delegate; // Not defined
template <class RetType, class... Args>
class Delegate<RetType(Args...)> : public DelegateBase {
public:
virtual RetType operator()(Args... args) = 0;
virtual Delegate* Clone() const = 0;
};
RetType
is the bound funciton return type. The Args
parameter pack is zero or more bound function arguments. operator()
invokes the bound function either synchronously or asynchronously depending on the derived class implementation.
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. DelegateMember<>
handles instance member functions. DelegateFree<>
handles free and static functions.
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 C, class R>
struct DelegateMember; // Not defined
template <class TClass, class RetType, class... Args>
class DelegateMember<TClass, RetType(Args...)> : public Delegate<RetType(Args...)> {
public:
typedef TClass* ObjectPtr;
typedef RetType(TClass::*MemberFunc)(Args...);
typedef RetType(TClass::*ConstMemberFunc)(Args...) const;
using ClassType = DelegateMember<TClass, RetType(Args...)>;
DelegateMember(ObjectPtr object, MemberFunc func) { Bind(object, func); }
DelegateMember(ObjectPtr object, ConstMemberFunc func) { Bind(object, func); }
DelegateMember() = delete;
/// Bind a member function to a delegate.
void Bind(ObjectPtr object, MemberFunc func) {
m_object = object;
m_func = func;
}
/// Bind a const member function to a delegate.
void Bind(ObjectPtr object, ConstMemberFunc func) {
m_object = object;
m_func = reinterpret_cast<MemberFunc>(func);
}
virtual DelegateMember* Clone() const override { return new DelegateMember(*this); }
// Invoke the bound delegate function
virtual RetType operator()(Args... args) override {
return std::invoke(m_func, m_object, args...);
}
virtual bool operator==(const DelegateBase& rhs) const override {
auto derivedRhs = dynamic_cast<const ClassType*>(&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 = nullptr; m_func = nullptr; }
explicit operator bool() const { return !Empty(); }
private:
ObjectPtr m_object = nullptr; // Pointer to a class object
MemberFunc m_func = nullptr; // Pointer to an instance member function
};
Notice std::invoke
is used to invoke the bound function within operator()
. With the RetVal
and Args
parameter pack template argument this single DelegateMember
class handles all target function signatures.
DelegateFree<>
binds to a free or static member function. Notice it inherits from Delegate<>
just like DelegateMember<>
. Bind()
takes a function pointer and operator()
allows subsequent invocation of the bound function.
template <class R>
struct DelegateFree; // Not defined
template <class RetType, class... Args>
class DelegateFree<RetType(Args...)> : public Delegate<RetType(Args...)> {
public:
typedef RetType(*FreeFunc)(Args...);
using ClassType = DelegateFree<RetType(Args...)>;
DelegateFree(FreeFunc func) { Bind(func); }
DelegateFree() = delete;
/// Bind a free function to the delegate.
void Bind(FreeFunc func) { m_func = func; }
virtual DelegateFree* Clone() const override { return new DelegateFree(*this); }
/// Invoke the bound delegate function.
virtual RetType operator()(Args... args) override {
return std::invoke(m_func, args...);
}
virtual bool operator==(const DelegateBase& rhs) const override {
auto derivedRhs = dynamic_cast<const ClassType*>(&rhs);
return derivedRhs &&
m_func == derivedRhs->m_func;
}
bool Empty() const { return !m_func; }
void Clear() { m_func = nullptr; }
explicit operator bool() const { return !Empty(); }
private:
FreeFunc m_func = nullptr; // Pointer to a free function
};
DelegateMemberAsync<>
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 DelegateMsgHeapArgs<>
instance for sending through the message queue using DispatchDelegate()
. After operator()
is called, the DelegateInvoke()
function is called by the target thread to actually invoke the bound function.
template <class C, class R>
struct DelegateMemberAsync; // Not defined
template <class TClass, class... Args>
class DelegateMemberAsync<TClass, void(Args...)> :
public DelegateMember<TClass, void(Args...)>, public IDelegateInvoker {
public:
typedef TClass* ObjectPtr;
typedef void (TClass::*MemberFunc)(Args...);
typedef void (TClass::*ConstMemberFunc)(Args...) const;
// Constructors take a class instance, member function, and callback thread
DelegateMemberAsync(ObjectPtr object, MemberFunc func, DelegateThread* thread) :
m_sync(false)
{ Bind(object, func, thread); }
DelegateMemberAsync(ObjectPtr object, ConstMemberFunc func, DelegateThread* thread) :
m_sync(false)
{ Bind(object, func, thread); }
DelegateMemberAsync() : m_thread(nullptr), m_sync(false) { }
/// Bind a member function to a delegate.
void Bind(ObjectPtr object, MemberFunc func, DelegateThread* thread) {
m_thread = thread;
DelegateMember<TClass, void(Args...)>::Bind(object, func);
}
/// Bind a const member function to a delegate.
void Bind(ObjectPtr object, ConstMemberFunc func, DelegateThread* thread) {
m_thread = thread;
DelegateMember<TClass, void(Args...)>::Bind(object, func);
}
virtual DelegateMemberAsync<TClass, void(Args...)>* Clone() const {
return new DelegateMemberAsync<TClass, void(Args...)>(*this);
}
virtual bool operator==(const DelegateBase& rhs) const {
const DelegateMemberAsync<TClass, void(Args...)>*
derivedRhs = dynamic_cast<const DelegateMemberAsync<TClass, void(Args...)>*>(&rhs);
return derivedRhs &&
m_thread == derivedRhs->m_thread &&
DelegateMember<TClass, void(Args...)>::operator == (rhs);
}
/// Invoke delegate function asynchronously
virtual void operator()(Args... args) {
if (m_thread == nullptr || m_sync)
DelegateMember<TClass, void(Args...)>::operator()(args...);
else
{
// Create a clone instance of this delegate
auto delegate =
std::shared_ptr<DelegateMemberAsync<TClass, void(Args...)>>(Clone());
// Create the delegate message
auto msg = std::shared_ptr<DelegateMsgHeapArgs<Args...>>
(new DelegateMsgHeapArgs<Args...>(delegate, args...));
// Dispatch message onto the callback destination thread. DelegateInvoke()
// will be called by the target thread.
m_thread->DispatchDelegate(msg);
}
}
/// Called by the target thread to invoke the delegate function
virtual void DelegateInvoke(std::shared_ptr<DelegateMsgBase> msg) {
// Typecast the base pointer to back to the templatized instance
auto delegateMsg = static_cast<DelegateMsgHeapArgs<Args...>*>(msg.get());
// Invoke the delegate function
m_sync = true;
std::apply(&DelegateMember<TClass, void(Args...)>::operator(),
std::tuple_cat(std::make_tuple(this), delegateMsg->GetArgs()));
}
private:
/// Target thread to invoke the delegate function
DelegateThread* m_thread;
bool m_sync;
};
Unlike the synchronous delegates that use std::invoke
, the asynchronous versions use std::apply
to invoke the bound function on the target thread with a tuple of arguments previously created by make_tuple_heap()
and sent through the message queue.
// Invoke the delegate function
m_sync = true;
std::apply(&DelegateMember<TClass, void(Args...)>::operator(),
std::tuple_cat(std::make_tuple(this), delegateMsg->GetArgs()));
DelegateMemberAsyncWait<>
is a blocking asynchronous delegate that binds to a class instance member function. The two main functions are shown below. When operator()
is called it blocks waiting for DelegateInvoke()
will be called on the target thread or the timeout to expire. The "Wait
" versions do not use make_tuple_heap()
as the original data types are directly passed to the target thread to support output arguments.
template <class C, class R>
struct DelegateMemberAsyncWait; // Not defined
template <class TClass, class RetType, class... Args>
class DelegateMemberAsyncWait<TClass, RetType(Args...)> : public DelegateMember<TClass, RetType(Args...)>, public IDelegateInvoker {
public:
/// ...
/// Invoke delegate function asynchronously
virtual RetType operator()(Args... args) override {
if (m_sync)
return BaseType::operator()(args...);
else
{
// Create a clone instance of this delegate
auto delegate = std::shared_ptr<ClassType>(Clone());
// Create a new message instance
auto msg = std::make_shared<DelegateMsg<Args...>>(delegate, args...);
// Dispatch message onto the callback destination thread. DelegateInvoke()
// will be called by the target thread.
m_thread.DispatchDelegate(msg);
// Wait for target thread to execute the delegate function
if ((m_success = delegate->m_sema.Wait(m_timeout)))
m_retVal = delegate->m_retVal;
if constexpr (std::is_void<RetType>::value == false)
{
if (m_retVal.has_value())
return std::any_cast<RetType>(m_retVal);
else
return RetType();
}
}
}
/// Invoke delegate function asynchronously
auto AsyncInvoke(Args... args)
{
if constexpr (std::is_void<RetType>::value == true)
{
operator()(args...);
return IsSuccess() ? std::optional<bool>(true) : std::optional<bool>();
}
else
{
auto retVal = operator()(args...);
return IsSuccess() ? std::optional<RetType>(retVal) : std::optional<RetType>();
}
}
/// Called by the target thread to invoke the delegate function
virtual void DelegateInvoke(std::shared_ptr<DelegateMsgBase> msg) override {
// Typecast the base pointer to back to the templatized instance
auto delegateMsg = std::dynamic_pointer_cast<DelegateMsg<Args...>>(msg);
if (delegateMsg == nullptr)
throw std::invalid_argument("Invalid DelegateMsg cast");
// Invoke the delegate function then signal the waiting thread
m_sync = true;
if constexpr (std::is_void<RetType>::value == true)
std::apply(&BaseType::operator(), std::tuple_cat(std::make_tuple(this), delegateMsg->GetArgs()));
else
m_retVal = std::apply(&BaseType::operator(), std::tuple_cat(std::make_tuple(this), delegateMsg->GetArgs()));
m_sema.Signal();
}
/// ...
Non-blocking asynchronous invocations means that all argument data must be copied into the heap for transport to the destination 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 created on the heap to ensure the data is valid on the destination thread. The key to being able to save each parameter into DelegateMsgHeapArgs<>
is the make_tuple_heap()
function. This template metaprogramming function creates a tuple
of arguments where each tuple element is created on the heap.
/// @brief Terminate the template metaprogramming argument loop
template<typename... Ts>
auto make_tuple_heap(std::list<std::shared_ptr<heap_arg_deleter_base>>& heapArgs,
std::tuple<Ts...> tup)
{
return tup;
}
/// @brief Creates a tuple with all tuple elements created on the heap using
/// operator new. Call with an empty list and empty tuple. The empty tuple is concatenated
/// with each heap element. The list contains heap_arg_deleter_base objects for each
/// argument heap memory block that will be automatically deleted after the bound
/// function is invoked on the target thread.
template<typename Arg1, typename... Args, typename... Ts>
auto make_tuple_heap(std::list<std::shared_ptr<heap_arg_deleter_base>>& heapArgs,
std::tuple<Ts...> tup, Arg1 arg1, Args... args)
{
auto new_tup = tuple_append(heapArgs, tup, arg1);
return make_tuple_heap(heapArgs, new_tup, args...);
}
Template metaprogramming uses the C++ template system to perform compile-time computations within the code. Notice the recursive compiler call of make_tuple_heap()
as the Arg1
template parameter gets consumed by the function until no arguments remain and the recursion is terminated. The snippet above shows the concatenation of heap allocated tuple function arguments. This allows for the arguments to be copied into dynamic memory for transport to the target thread through a message queue.
This bit of code inside make_tuple_heap.h was tricky to create in that each argument must have memory allocated, data copied, appended to the tuple, then subsequently deallocated all based on its type. To further complicate things, this all has to be done generically with N number of disparte template argument parameters. This was the key to getting a template parameter pack of arguments through a message queue. DelegateMsgHeapArgs
then stores the tuple parameters for easy usage by the target thread. The target thread uses std::apply()
to invoke the bound function with the heap allocated tuple argument(s).
The pointer argument tuple_append()
implementation is shown below. It creates dynamic memory for the argument, argument data copied, adds to a deleter list for subsequent later cleanup after the target function is invoked, and finally returns the appended tuple.
/// @brief Append a pointer argument to the tuple
template <typename Arg, typename... TupleElem>
auto tuple_append(std::list<std::shared_ptr<heap_arg_deleter_base>>& heapArgs,
const std::tuple<TupleElem...> &tup, Arg* arg)
{
Arg* heap_arg = nullptr;
try
{
heap_arg = new Arg(*arg);
std::shared_ptr<heap_arg_deleter_base> deleter(new heap_arg_deleter<Arg*>(heap_arg));
heapArgs.push_back(deleter);
return std::tuple_cat(tup, std::make_tuple(heap_arg));
}
catch (std::bad_alloc&)
{
if (heap_arg)
delete heap_arg;
throw;
}
}
The pointer argument deleter is implemented below. When the target function invocation is complete, the heap_arg_deleter
destructor will delete
the heap argument memory. The heap argument cannot be a changed to a smart pointer because it would change the argument type used in the target function signature. Therefore, the heap_arg_deleter
is used as a smart pointer wrapper around the (potentially) non-smart heap argument.
/// @brief Frees heap memory for pointer heap argument
template<typename T>
class heap_arg_deleter<T*> : public heap_arg_deleter_base
{
public:
heap_arg_deleter(T* arg) : m_arg(arg) { }
virtual ~heap_arg_deleter()
{
delete m_arg;
}
private:
T* m_arg;
};
Non-blocking asynchronous invocations means that all argument data must be copied into the heap for transport to the destination thread. This means all arguments, regardless of the argument type, will be duplicated including: value, pointer, pointer to pointer, reference. 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.
For instance, invoking this function asynchronously the argument TestStruct
will be copied.
void TestFunc(TestStruct* data);
Occasionally, you may not want the delegate library to copy your arguments. Instead, you just want the destination thread to have a pointer to the original copy. Here is how to really send a pointer without duplicating the object pointed to. Use a shared_ptr
as the function argument prevents object copying.
For instance, invoking this function asynchronously will not copy the TestStruct
object.
void TestFunc(std::shared_ptr<TestStruct> data);
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
. Since the array size is not known, the library cannot copy the entire array. 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. My recommendation is to avoid C-style arrays if possible when using asynchronous delegates to avoid confusion and mistakes.
The std::thread
implemented thread loop is shown below. The loop calls the DelegateInvoke()
function on each asynchronous delegate instance.
void WorkerThread::Process()
{
while (1)
{
std::shared_ptr<ThreadMsg> msg;
{
// Wait for a message to be added to the queue
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);
// Convert the ThreadMsg void* data back to a DelegateMsg*
auto delegateMsg = msg->GetData();
// Invoke the callback on the target thread
delegateMsg->GetDelegateInvoker()->DelegateInvoke(delegateMsg);
break;
}
case MSG_EXIT_THREAD:
{
return;
}
default:
ASSERT();
}
}
}
Any project-specific thread loop can call DelegateInvoke()
. This is just one example. The only requirement is that your worker thread class inherit from DelegateLib::DelegateThread
and implement the DispatchDelegate()
abstract function. DisplatchDelegate()
will insert the shared message pointer into the thread queue for processing.
Delegate containers store one or more delegates. The delegate container hierarchy is shown below:
MulticastDelegate<>
MulticastDelegateSafe<>
SinglecastDelegate<>
MulticastDelegate<>
provides the function operator()
to sequentially invoke each delegate within the list.
MulticastDelegateSafe<>
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 <class R>
struct MulticastDelegateSafe; // Not defined
/// @brief Thread-safe multicast delegate container class.
template<class RetType, class... Args>
class MulticastDelegateSafe<RetType(Args...)> : public MulticastDelegate<RetType(Args...)>
{
public:
MulticastDelegateSafe() { LockGuard::Create(&m_lock); }
~MulticastDelegateSafe() { LockGuard::Destroy(&m_lock); }
void operator+=(const Delegate<RetType(Args...)>& delegate) {
LockGuard lockGuard(&m_lock);
MulticastDelegate<RetType(Args...)>::operator +=(delegate);
}
void operator-=(const Delegate<RetType(Args...)>& delegate) {
LockGuard lockGuard(&m_lock);
MulticastDelegate<RetType(Args...)>::operator -=(delegate);
}
void operator()(Args... args) {
LockGuard lockGuard(&m_lock);
MulticastDelegate<RetType(Args...)>::operator ()(args...);
}
bool Empty() {
LockGuard lockGuard(&m_lock);
return MulticastDelegate<RetType(Args...)>::Empty();
}
void Clear() {
LockGuard lockGuard(&m_lock);
MulticastDelegate<RetType(Args...)>::Clear();
}
explicit operator bool() {
LockGuard lockGuard(&m_lock);
return MulticastDelegate<RetType(Args...)>::operator bool();
}
private:
// Prevent copying objects
MulticastDelegateSafe(const MulticastDelegateSafe&) = delete;
MulticastDelegateSafe& operator=(const MulticastDelegateSafe&) = delete;
/// Lock to make the class thread-safe
LOCK m_lock;
};
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:
/// Clients register with MulticastDelegateSafe1 to get callbacks when system mode changes
MulticastDelegateSafe<void(const SystemModeChanged&)> SystemModeChangedDelegate;
/// Get singleton instance of this class
static SysData& GetInstance();
/// Sets the system mode and notify registered clients via SystemModeChangedDelegate.
/// @param[in] systemMode - the new system mode.
void SetSystemMode(SystemMode::Type systemMode);
private:
SysData();
~SysData();
/// The current system mode data
SystemMode::Type m_systemMode;
/// Lock to make the class thread-safe
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);
// Create the callback data
SystemModeChanged callbackData;
callbackData.PreviousSystemMode = m_systemMode;
callbackData.CurrentSystemMode = systemMode;
// Update the system mode
m_systemMode = systemMode;
// Callback all registered subscribers
if (SystemModeChangedDelegate)
SystemModeChangedDelegate(callbackData);
}
SysDataClient
is a delegate subscriber and registers for SysData::SystemModeChangedDelegate
notifications within the constructor.
// Constructor
SysDataClient() :
m_numberOfCallbacks(0)
{
// Register for async delegate callbacks
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.
// Set new SystemMode values. Each call will invoke callbacks to all
// registered client subscribers.
SysData::GetInstance().SetSystemMode(SystemMode::STARTING);
SysData::GetInstance().SetSystemMode(SystemMode::NORMAL);
SysDataNoLock
is an alternate implementation that uses a private
MulticastDelegateSafe<>
for setting the system mode asynchronously and without locks.
class SysDataNoLock
{
public:
/// Clients register with MulticastDelegateSafe to get callbacks when system mode changes
MulticastDelegateSafe<void(const SystemModeChanged&)> SystemModeChangedDelegate;
/// Get singleton instance of this class
static SysDataNoLock& GetInstance();
/// Sets the system mode and notify registered clients via SystemModeChangedDelegate.
/// @param[in] systemMode - the new system mode.
void SetSystemMode(SystemMode::Type systemMode);
/// Sets the system mode and notify registered clients via a temporary stack created
/// asynchronous delegate.
/// @param[in] systemMode - The new system mode.
void SetSystemModeAsyncAPI(SystemMode::Type systemMode);
/// Sets the system mode and notify registered clients via a temporary stack created
/// asynchronous delegate. This version blocks (waits) until the delegate callback
/// is invoked and returns the previous system mode value.
/// @param[in] systemMode - The new system mode.
/// @return The previous system mode.
SystemMode::Type SetSystemModeAsyncWaitAPI(SystemMode::Type systemMode);
private:
SysDataNoLock();
~SysDataNoLock();
/// Private callback to get the SetSystemMode call onto a common thread
MulticastDelegateSafe<void(SystemMode::Type)> SetSystemModeDelegate;
/// Sets the system mode and notify registered clients via SystemModeChangedDelegate.
/// @param[in] systemMode - the new system mode.
void SetSystemModePrivate(SystemMode::Type);
/// The current system mode data
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)
{
// Invoke the private callback. SetSystemModePrivate() will be called on workerThread2.
SetSystemModeDelegate(systemMode);
}
Since this private
function is always invoked asynchronously on workerThread2
, it doesn 't require locks.
void SysDataNoLock::SetSystemModePrivate(SystemMode::Type systemMode)
{
// Create the callback data
SystemModeChanged callbackData;
callbackData.PreviousSystemMode = m_systemMode;
callbackData.CurrentSystemMode = systemMode;
// Update the system mode
m_systemMode = systemMode;
// Callback all registered subscribers
if (SystemModeChangedDelegate)
SystemModeChangedDelegate(callbackData);
}
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)
{
// Is the caller executing on workerThread2?
if (workerThread2.GetThreadId() != WorkerThread::GetCurrentThreadId())
{
// Create an asynchronous delegate and re-invoke the function call on workerThread2
auto delegate =
MakeDelegate(this, &SysDataNoLock::SetSystemModeAsyncAPI, workerThread2);
delegate(systemMode);
return;
}
// Create the callback data
SystemModeChanged callbackData;
callbackData.PreviousSystemMode = m_systemMode;
callbackData.CurrentSystemMode = systemMode;
// Update the system mode
m_systemMode = systemMode;
// Callback all registered subscribers
if (SystemModeChangedDelegate)
SystemModeChangedDelegate(callbackData);
}
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)
{
// Is the caller executing on workerThread2?
if (workerThread2.GetThreadId() != WorkerThread::GetCurrentThreadId())
{
// Create an asynchronous delegate and re-invoke the function call on workerThread2
auto delegate =
MakeDelegate(this, &SysDataNoLock::SetSystemModeAsyncWaitAPI,
workerThread2, WAIT_INFINITE);
return delegate(systemMode);
}
// Create the callback data
SystemModeChanged callbackData;
callbackData.PreviousSystemMode = m_systemMode;
callbackData.CurrentSystemMode = systemMode;
// Update the system mode
m_systemMode = systemMode;
// Callback all registered subscribers
if (SystemModeChangedDelegate)
SystemModeChangedDelegate(callbackData);
return callbackData.PreviousSystemMode;
}
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 SinglecastDelegate
used inside a Timer
class solves this nicely.
/// @brief A timer class provides periodic timer callbacks on the client's
/// thread of control. Timer is thread safe.
class Timer
{
public:
/// Client's register with Expired to get timer callbacks
SinglecastDelegate<void(void)> Expired;
/// Starts a timer for callbacks on the specified timeout interval.
/// @param[in] timeout - the timeout in milliseconds.
void Start(std::chrono::milliseconds timeout);
/// Stops a timer.
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);
The current master branch build passes all unit tests and Valgrind memory tests.
Build the project with ENABLE_UNIT_TESTS
build option to enable unit tests. See CMakeLists.txt
for more build option information.
Valgrind dynamic storage allocation tests were performed using the heap and fixed block allocator builds. Valgrind is a programming tool for detecting memory leaks, memory errors, and profiling performance in applications, primarily for Linux-based systems. All tests run on Linux.
The delegate library Valgrind test results using the heap.
==1779805== HEAP SUMMARY:
==1779805== in use at exit: 0 bytes in 0 blocks
==1779805== total heap usage: 923,465 allocs, 923,465 frees, 50,733,258 bytes allocated
==1779805==
==1779805== All heap blocks were freed -- no leaks are possible
==1779805==
==1779805== For lists of detected and suppressed errors, rerun with: -s
==1779805== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)
Test results with the ENABLE_ALLOCATOR
fixed block allocator build option enabled. Notice the fixed block runtime uses 22MB verses 50MB for the heap build. Heap storage recycling mode was used by the fixed block allocator. See stl_allocator and xallocator for information about the memory allocators.
==1780037== HEAP SUMMARY:
==1780037== in use at exit: 0 bytes in 0 blocks
==1780037== total heap usage: 644,606 allocs, 644,606 frees, 22,091,451 bytes allocated
==1780037==
==1780037== All heap blocks were freed -- no leaks are possible
==1780037==
==1780037== For lists of detected and suppressed errors, rerun with: -s
==1780037== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)
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);
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, std::chrono::milliseconds(5000));
Delegates are added/removed from multicast containers using operator+=
and operator-=
. All containers accept all delegate types.
MulticastDelegate<void(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.
MulticastDelegateSafe<void(int)> multicastContainer;
multicastContainer += MakeDelegate(&MyFreeFunc, myThread);
multicastContainer -= MakeDelegate(&MyFreeFunc, myThread);
Single cast delegates are added and removed using operator=
.
SinglecastDelegate<void(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. 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.
- 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
- 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
- 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
- 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)
- Integration Test Framework using Google Test and Delegates - by David Lafreniere
- C++ State Machine with Asynchronous Multicast Delegates - by David Lafreniere
- Asynchronous Multicast Delegates in C++ - by David Lafreniere
- Remote Procedure Calls using C++ Delegates - by David Lafreniere
- Asynchronous Multicast Callbacks in C - by David Lafreniere
- Asynchronous Multicast Callbacks with Inter-Thread Messaging - by David Lafreniere
- Type-Safe Multicast Callbacks in C - by David Lafreniere
- C++ std::thread Event Loop with Message Queue and Timer - by David Lafreniere
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 modern 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.