A C++ delegate library capable of invoking any callable function either synchronously or asynchronously on a user specified thread of control.
Asynchronous function calls support both non-blocking and blocking modes with a timeout. The library supports all types of target functions, including free functions, class member functions, static class functions, lambdas, and std::function
. It is capable of handling any function signature, regardless of the number of arguments or return value. All argument types are supported, including by value, pointers, pointers to pointers, and references. The delegate library takes care of the intricate details of function invocation across thread boundaries. Thread-safe delegate containers stores delegate instances with a matching function signature.
Originally published on CodeProject at: Asynchronous Multicast Delegates in Modern C++
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.
A example delegate container inserting and removing all delegate types.
WorkerThread workerThread1("WorkerThread1");
static int callCnt = 0;
void FreeFunc(int value) {
cout << "FreeFunc " << value << " " << ++callCnt << endl;
}
// Simple test invoking all target types
void TestAllTargetTypes() {
class Class {
public:
static void StaticFunc(int value) {
cout << "StaticFunc " << value << " " << ++callCnt << endl;
}
void MemberFunc(int value) {
cout << "MemberFunc " << value << " " << ++callCnt << endl;
}
void MemberFuncConst(int value) const {
cout << "MemberFuncConst " << value << " " << ++callCnt << endl;
}
};
int stackVal = 100;
std::function<void(int)> LambdaCapture = [stackVal](int i) {
std::cout << "LambdaCapture " << i + stackVal << " " << ++callCnt << endl;
};
std::function<void(int)> LambdaNoCapture = [](int i) {
std::cout << "LambdaNoCapture " << i << " " << ++callCnt << endl;
};
std::function<void(int)> LambdaForcedCapture = +[](int i) {
std::cout << "LambdaForcedCapture " << i << " " << ++callCnt << endl;
};
Class testClass;
std::shared_ptr<Class> testClassSp = std::make_shared<Class>();
// Create a multicast delegate container that accepts Delegate<void(int)> delegates.
// Any function with the signature "void Func(int)".
MulticastDelegateSafe<void(int)> delegateA;
// Add all callable function targets to the delegate container
// Synchronous delegates
delegateA += MakeDelegate(&FreeFunc);
delegateA += MakeDelegate(LambdaCapture);
delegateA += MakeDelegate(LambdaNoCapture);
delegateA += MakeDelegate(LambdaForcedCapture);
delegateA += MakeDelegate(&Class::StaticFunc);
delegateA += MakeDelegate(&testClass, &Class::MemberFunc);
delegateA += MakeDelegate(&testClass, &Class::MemberFuncConst);
delegateA += MakeDelegate(testClassSp, &Class::MemberFunc);
delegateA += MakeDelegate(testClassSp, &Class::MemberFuncConst);
// Asynchronous delegates
delegateA += MakeDelegate(&FreeFunc, workerThread1);
delegateA += MakeDelegate(LambdaCapture, workerThread1);
delegateA += MakeDelegate(LambdaNoCapture, workerThread1);
delegateA += MakeDelegate(LambdaForcedCapture, workerThread1);
delegateA += MakeDelegate(&Class::StaticFunc, workerThread1);
delegateA += MakeDelegate(&testClass, &Class::MemberFunc, workerThread1);
delegateA += MakeDelegate(&testClass, &Class::MemberFuncConst, workerThread1);
delegateA += MakeDelegate(testClassSp, &Class::MemberFunc, workerThread1);
delegateA += MakeDelegate(testClassSp, &Class::MemberFuncConst, workerThread1);
// Asynchronous blocking delegates
delegateA += MakeDelegate(&FreeFunc, workerThread1, WAIT_INFINITE);
delegateA += MakeDelegate(LambdaCapture, workerThread1, WAIT_INFINITE);
delegateA += MakeDelegate(LambdaNoCapture, workerThread1, WAIT_INFINITE);
delegateA += MakeDelegate(LambdaForcedCapture, workerThread1, WAIT_INFINITE);
delegateA += MakeDelegate(&Class::StaticFunc, workerThread1, WAIT_INFINITE);
delegateA += MakeDelegate(&testClass, &Class::MemberFunc, workerThread1, WAIT_INFINITE);
delegateA += MakeDelegate(&testClass, &Class::MemberFuncConst, workerThread1, WAIT_INFINITE);
delegateA += MakeDelegate(testClassSp, &Class::MemberFunc, workerThread1, WAIT_INFINITE);
delegateA += MakeDelegate(testClassSp, &Class::MemberFuncConst, workerThread1, WAIT_INFINITE);
// Invoke all callable function targets stored within the delegate container
if (delegateA)
delegateA(123);
// Wait for async callbacks to complete
std::this_thread::sleep_for(std::chrono::milliseconds(100));
// Remove all callable function targets from the delegate container
// Synchronous delegates
delegateA -= MakeDelegate(&FreeFunc);
delegateA -= MakeDelegate(LambdaCapture);
delegateA -= MakeDelegate(LambdaNoCapture);
delegateA -= MakeDelegate(LambdaForcedCapture);
delegateA -= MakeDelegate(&Class::StaticFunc);
delegateA -= MakeDelegate(&testClass, &Class::MemberFunc);
delegateA -= MakeDelegate(&testClass, &Class::MemberFuncConst);
delegateA -= MakeDelegate(testClassSp, &Class::MemberFunc);
delegateA -= MakeDelegate(testClassSp, &Class::MemberFuncConst);
// Asynchronous delegates
delegateA -= MakeDelegate(&FreeFunc, workerThread1);
delegateA -= MakeDelegate(LambdaCapture, workerThread1);
delegateA -= MakeDelegate(LambdaNoCapture, workerThread1);
delegateA -= MakeDelegate(LambdaForcedCapture, workerThread1);
delegateA -= MakeDelegate(&Class::StaticFunc, workerThread1);
delegateA -= MakeDelegate(&testClass, &Class::MemberFunc, workerThread1);
delegateA -= MakeDelegate(&testClass, &Class::MemberFuncConst, workerThread1);
delegateA -= MakeDelegate(testClassSp, &Class::MemberFunc, workerThread1);
delegateA -= MakeDelegate(testClassSp, &Class::MemberFuncConst, workerThread1);
// Asynchronous blocking delegates
delegateA -= MakeDelegate(&FreeFunc, workerThread1, WAIT_INFINITE);
delegateA -= MakeDelegate(LambdaCapture, workerThread1, WAIT_INFINITE);
delegateA -= MakeDelegate(LambdaNoCapture, workerThread1, WAIT_INFINITE);
delegateA -= MakeDelegate(LambdaForcedCapture, workerThread1, WAIT_INFINITE);
delegateA -= MakeDelegate(&Class::StaticFunc, workerThread1, WAIT_INFINITE);
delegateA -= MakeDelegate(&testClass, &Class::MemberFunc, workerThread1, WAIT_INFINITE);
delegateA -= MakeDelegate(&testClass, &Class::MemberFuncConst, workerThread1, WAIT_INFINITE);
delegateA -= MakeDelegate(testClassSp, &Class::MemberFunc, workerThread1, WAIT_INFINITE);
delegateA -= MakeDelegate(testClassSp, &Class::MemberFuncConst, workerThread1, WAIT_INFINITE);
ASSERT_TRUE(delegateA.Size() == 0);
ASSERT_TRUE(callCnt == 27);
}
SetSystemModeAsyncAPI()
is an asynchronous function call that re-invokes on workerThread2
if necessary.
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
MakeDelegate(this, &SysDataNoLock::SetSystemModeAsyncAPI, workerThread2).AsyncInvoke(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);
}
Primary delegate library classes.
// Delegates
DelegateBase
Delegate<>
DelegateFree<>
DelegateFreeAsync<>
DelegateFreeAsyncWait<>
DelegateMember<>
DelegateMemberAsync<>
DelegateMemberAsyncWait<>
DelegateMemberSp<>
DelegateMemberSpAsync<>
DelegateMemberSpAsyncWait<>
DelegateFunction<>
DelegateFunctionAsync<>
DelegateFunctionAsyncWait<>
// Delegate Containers
SinglecastDelegate<>
MulticastDelegate<>
MulticastDelegateSafe<>
// Helper Classes
IDelegateInvoker
DelegateMsg
DelegateThread
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 build -S .
cmake -G "Visual Studio 17 2022" -A x64 -B build -S .
cmake -G "Visual Studio 17 2022" -A x64 -B build -S . -DENABLE_ALLOCATOR=ON
After executed, open the Visual Studio project from within the build
directory.
cmake -G "Unix Makefiles" -B build -S .
cmake -G "Unix Makefiles" -B build -S . -DENABLE_ALLOCATOR=ON
After executed, build the software from within the build
directory using the command make
. Run the console app using ./DelegateApp
.
See Design Details for implementation design details and more examples.
See Doxygen main page located at doxygen/html/index.html
for source code documentation. Clone the repository to view the pages in a browser.
Alternative asynchronous implementations similar in concept to C++ delegate.
- Asynchronous Callbacks in C++ - A C++ asynchronous callback framework simplifies passing data between threads.
- Asynchronous Callbacks in C - A C language asynchronous callback framework simplifies passing data between threads.
Supporting source code used within the delegate library.
- C++ std::thread Event Loop - C++ std::thread Event Loop with Message Queue and Timer.
- Fixed Block std::allocator - STL std::allocator Fixed Block Memory Allocator.
Repositories utilizing the delegate library within 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.
Find this repository useful? Consider giving it a star!