Asynchronous multicast delegates in modern C++

introduce

Few things seem to interest C++ programmers more than delegates. In other languages, delegates are a first-class feature, so developers can use these well-understood constructs. However, in C++, delegates are not natively available. However, that doesn't stop us programmers from trying to emulate the ease of delegating storage and calling any callable function.

Delegates generally support synchronous execution, that is, when called; the bound function executes in the caller's thread of control. In multithreaded applications, it is desirable to specify the target function and the thread it should execute on without imposing function signature restrictions. This library does the heavy lifting of sending the delegate and all parameter data to the target thread. The idea behind this article is to provide a C++ delegate library with a consistent API that enables both synchronous and asynchronous calls to any callable function.

A modern C++ delegate library is characterized by:

  1. Any Compiler - Standard C++17 code that works with any compiler, no weird hacks
  2. Any function - call any callable function: member function, static function or free function
  3. Any parameter type - supports any parameter type: value, reference, pointer, pointer to pointer
  4. Multiple Parameters - Supports N function parameters for bound functions
  5. Synchronous call - call the bound function synchronously
  6. Asynchronous call - call the bound function asynchronously on the thread specified by the client
  7. Blocking asynchronous calls - call asynchronously using blocking or non-blocking delegates
  8. Smart pointer support - use raw object pointers or bound instance functionsstd::shared_ptr
  9. Automatic heap handling - automatically copy parameter data to the heap for safe transfer via message queue
  10. Any OS - Easily port to any OS. Includes C++11 std::threadport
  11. Visual Studio and Eclipse  - includes VC++ and GCC projects
  12. Unit Tests - Extensive unit tests including delegate libraries
  13. No external libraries - the delegate does not depend on external libraries
  14. Ease of use - function signature template parameters (eg, MulticastDelegate<void(TestStruct*)>)

The delegate implementation greatly simplifies multi-threaded application development by executing the delegate function with all function parameters on the thread of control that you specify. The framework handles all the low-level mechanisms to safely call any function signature on the target thread. Windows 2017 and Eclipse projects are included for easy experimentation.

representative background

If you're not familiar with delegates, the concept is pretty simple. A delegate can be thought of as a super function pointer. In C++, there is no pointer type capable of pointing to all possible variants of a function: instance member, virtual, const, static, and free (global). Function pointers cannot point to instance member functions, and pointers to member functions have various restrictions. However, the delegate class can point to any function in a type-safe manner if the function signatures match. In short, delegates point to any function with a matching signature to support anonymous function calls.

In fact, while delegation is useful, the multicast version extends its usefulness significantly. The ability to bind multiple function pointers and call all registries sequentially constitutes an efficient 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 multi-threaded systems, whether they are delegate-based or function pointer-based, is that the callbacks happen synchronously. Care must be taken that callbacks from another thread of control will not be invoked on code that is not thread-safe. Multithreaded application development is hard. It's hard for the original designer; it's hard because engineers of all skill levels have to maintain the code; it's hard because bugs manifest themselves in hard ways. Ideally, architectural solutions help minimize errors and simplify application development.

This C++ delegate implementation is fully functional, allowing any function, even instance member functions, to be called synchronously or asynchronously with any parameters. The delegate library makes binding and calling any function a breeze.

use code

I'll start by showing how to use the code, and then go into implementation details.

The delegate library consists of delegates and delegate containers. A delegate can be bound to a single callable function. A multicast delegate container keeps one or more delegates in a list to be called sequentially. A cast delegate container can accommodate at most one delegate.

The main delegate classes are listed below:

  • DelegateFree<>
  • DelegateFreeAsync<>
  • DelegateFreeAsyncWait<>
  • DelegateMember<>
  • DelegateMemberAsync<>
  • DelegateMemberAsyncWait<>
  • DelegateMemberSp<>
  • DelegateMemberSpAsync<>

DelegateFree<>Binding to a free or static member function. DelegateMember<> Bind to a class instance member function. DelegateMemberSp<>Use std::shared_ptrinstead of raw object pointers to bind to class instance member functions. All versions provide synchronous function calls.

DelegateFreeAsync<>, DelegateMemberAsync<>and DelegateMemberSpAsync<>operates in the same manner as their synchronous counterparts; except these versions provide non-blocking asynchronous function execution on a designated thread of control.

DelegateFreeAsyncWait<>And DelegateMemberAsyncWait<>provides blocking asynchronous function execution on the target thread, the caller provides a maximum wait timeout.

The three main delegate container classes are:

  • SinglecastDelegate<>
  • MulticastDelegate<>
  • MulticastDelegateSafe<>

SinglecastDelegate<>is a delegate container that accepts a single delegate. The advantage of the single conversion version is that it is slightly smaller and allows return types instead of voidbound functions.

MulticastDelegate<>is a delegate container, implemented as a singly linked list accepting multiple delegates. Only voiddelegates bound to functions with a return type can be added to a multicast delegate container.

MultcastDelegateSafe<>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 delegates by value. This means that delegates are copied internally to the heap or fixed block memory, depending on the mode. Users don't need to manually create delegates on the heap before inserting containers. Typically, overloaded template functions MakeDelegate() are used to create delegate instances from function parameters.

synchronous delegate

All delegates are MakeDelegate()created using overloaded template functions. The compiler uses template argument deduction to choose the correct MakeDelegate()version, eliminating the need to manually specify template arguments. For example, here's a simple free function.

To bind a free function to a delegate, use DelegateFree<void(int)>the MakeDelegate().template  DelegateFree parameter which is the signature of the complete function: void(int).returns  MakeDelegate()an DelegateFree<void(int)>object and the next line FreeFuncIntcalls the function using the delegate.

Member functions are bound to delegates in the same way, only this time MakeDelegate()with two parameters: the class instance and the member function pointer. The two DelegateMember template parameters are the class name (ie TestClass) and the bound function signature (ie void(TestStruct*)).

Instead of creating concrete free or member delegates, it is common to use a delegate container to hold one or more delegates. A delegate container can hold any delegate type. void (int)For example, a multicast delegate container bound to any function with a function signature looks like this:

A single cast delegate is created in the same way:

Function signatures that return values ​​are also possible. A delegate container takes float a function that takes one parameter and returns a int.

Guess you like

Origin blog.csdn.net/wouderw/article/details/128028805