委托通常支持同步执行,也就是说,当被调用时,绑定函数在调用者的控制线程中执行。在多线程应用程序上,最好指定目标函数和它应该在其上执行的线程,而不会施加函数签名限制。该库完成了将委托和所有参数数据获取到目标线程的繁重工作。本文背后的想法是提供一个具有一致 API 的 C++ 委托库,该 API 能够对任何可调用函数进行同步和异步调用。
委托库的特点是:
- Any Compiler – 任何编译器的标准 C++ 代码,没有奇怪的 hack
- 任何函数——调用任何可调用函数:成员、静态或自由
- 任何参数类型——支持任何参数类型:值、引用、指针、指向指针的指针
- 多个参数- 支持多个函数参数
- 类函数模板参数- 2022 年库更新中的新增功能
- 同步调用——同步调用绑定函数
- 异步调用——在客户端指定的线程上异步调用绑定函数
- 阻塞异步调用- 使用阻塞或非阻塞委托异步调用
- 智能指针支持- 使用原始对象指针绑定实例函数或
std::shared_ptr
- 自动堆处理——自动将参数数据复制到堆中,以便通过消息队列进行安全传输
- 固定块分配器- 可选择将堆分配转移到固定块内存池
- 任何操作系统——轻松移植到任何操作系统。包括Win32 和
std::thread
端口 - Visual Studio 和 Eclipse - 包括 VC++ 和 GCC 项目
- 单元测试- 包括委托库的广泛单元测试
- 没有外部库——委托不依赖外部库
- 易用性
FastDelegate
——尽可能匹配API
委托实现通过在您指定的控制线程上使用所有函数参数执行委托函数,显着简化了多线程应用程序的开发。该框架处理所有低级机制以安全地调用目标线程上的任何函数签名。
包括 Windows 2008、2015 和 Eclipse 项目以便于实验。虽然 Windows 操作系统提供线程、锁和消息队列,但代码被分区以便于移植到其他嵌入式或基于 PC 的系统。构建std::thread
版本意味着任何支持 C++ 标准库线程 API 的 C++11 编译器都能够使用委托,而无需进行移植工作。
提供了三种异步多播回调实现:两种用 C++ 编写,一种用 C 编写。其他两篇相关文章请参见参考资料部分。
使用 C++ 委托的远程过程调用 (RPC) 将此库扩展为包括进程间和处理器间通信。请参阅此相关文章的参考资料部分。
我创建了四个版本的“异步回调”想法;三个 C++ 版本和一个 C 版本。有关其他实现的链接,请参阅文章末尾的参考资料部分。
2022 年图书馆更新
C++ 委托库已更新,具有以下功能:
- 类函数委托语法
AsyncInvoke()
简化异步函数调用- 需要 C++11 或更高版本的 C++ 编译器
旧语法与新语法比较如下。下面的旧语法使用标准模板参数。它还要求使用数字或函数参数作为委托类型的一部分(例如, DelegateFree1<>
是一个函数参数委托)。
新语法使用类似函数的模板参数来提高可读性:
首选 2022 更新版本的库。
- 下载 AsyncMulticastDelegateCpp11.zip - 新的 2022 C++11 及更高版本。
- 下载 AsyncMulticastDelegate.zip - 原始 C++03 及更高版本。
本文使用旧语法作为示例。然而,解释是准确的。
代表背景
如果你不熟悉委托,这个概念很简单。委托可以被认为是一个超级函数指针。在 C++ 中,没有指针类型能够指向所有可能的函数变体:实例成员、虚拟、常量、静态和自由(全局)。函数指针不能指向实例成员函数,指向成员函数的指针有各种限制。但是,委托类可以以类型安全的方式指向只要函数签名匹配的任何函数。简而言之,委托指向具有匹配签名的任何函数以支持匿名函数调用。
最著名的 C++ 委托实现可能是FastDelegate
Doug Clugston 的。我在许多不同的项目中都成功地使用了这段代码。它很容易使用,而且我还没有找到一个它不能工作的编译器。
虽然使用FastDelegate
是无缝的(而且速度很快!),但对代码的检查发现了许多黑客和“可怕的”强制转换,使其可以在不同的编译器中普遍工作。当我第一次研究源代码时,由于其复杂性,我几乎没有将其仅用于外观的项目。然而,这让我开始思考;说我不太关心速度。是否有可能设计一个符合 C++ 标准的委托?如果是这样,它可以匹配的界面和可用性FastDelegate
?
在实践中,虽然委托很有用,但多播版本显着扩展了它的实用性。绑定多个函数指针并顺序调用所有注册商的能力构成了一种有效的发布者/订阅者机制。发布者代码公开一个委托容器,并且一个或多个匿名订阅者向发布者注册以获取回调通知。
多线程系统上的回调问题,无论是基于委托还是基于函数指针,都在于回调同步发生。必须注意不要在非线程安全的代码上调用来自另一个控制线程的回调。多线程应用程序开发很难。原设计师很难;这很难,因为不同技能水平的工程师必须维护代码;这很难,因为错误以困难的方式表现出来。理想情况下,架构解决方案有助于最大限度地减少错误并简化应用程序开发。
有些人可能会质疑为什么std::function
不将其用作异步委托的基础。最初,我开始实现一个std::function
用于定位任何可调用函数的版本,它工作除了一个关键特性:平等。我很快发现您无法比较std::function
从容器中注销所必需的相等性。似乎没有简单、通用的方法来解决这个问题。如果没有删除之前添加的可调用函数的方法,设计就失败了。一切都没有丢失。我最终创建的委托层次结构实际上最终成为我试图完成的功能集的一个优势。另外,创作很有趣。
我在 CodeProject 上写的题为“ Asynchronous Multicast Callbacks with Inter-Thread Messaging ”的文章提供了一个异步多播回调,在概念上与这里提出的类似,但回调签名是固定的,并且只支持一个模板化函数参数。它还将回调函数类型限制为静态成员或自由函数。不支持实例成员函数。接受这些限制的好处是AsycnCallback<>
实现更加简单和紧凑。
此 C++ 委托实现功能齐全,允许使用任何参数同步或异步调用任何函数,甚至是实例成员函数。该delegate
库使绑定和调用任何函数变得轻而易举。
使用代码
我将首先介绍如何使用代码,然后介绍实现细节。
委托库由委托和委托容器组成。委托能够绑定到单个可调用函数。多播委托容器在列表中保存一个或多个委托,以便按顺序调用。单个演员委托容器最多可容纳一个委托。
下面列出了主要的委托类,其中X
是目标函数签名中的参数数量。例如,如果目标签名使用一个参数,例如 in void (int)
,则使用DelegateFree1<>
版本。类似地,如果在 中使用三个参数void (int, float, char)
,DelgateFree3<>
则使用 。
DelegateFreeX<>
DelegateFreeAsyncX<>
DelegateFreeAsyncWaitX<>
DelegateMemberX<>
DelegateMemberAsyncX<>
DelegateMemberAsyncWaitX<>
DelegateMemberSpX<>
DelegateMemberSpAsyncX<>
DelegateRemoteSendX<>
DelegateFreeRemoteRecvX<>
DelegateMemberRemoteRecvX<>
DelegateFreeX<>
绑定到自由或静态成员函数。DelegateMemberX<>
绑定到类实例成员函数。使用 a而不是原始对象指针DelegateMemberSpX<>
绑定到类实例成员函数。std::shared_ptr
所有版本都提供同步函数调用。
DelegateFreeAsyncX<>
,DelegateMemberAsyncX<>
并DelegateMemberSpAsyncX<>
以与其同步对应物相同的方式操作;除了这些版本在指定的控制线程上提供非阻塞异步函数执行。
DelegateFreeAsyncWaitX<>
并DelegateMemberAsyncWaitX<>
在目标线程上提供阻塞异步函数执行,调用者提供的最大等待超时。
DelegateRemoteSendX<>
,DelegateFreeRemoteRecvX<>
并在使用 C++ 委托的远程过程调用DelegateMemberRemoteRecvX<>
一文中进行了解释。
三个主要的委托容器类是:
SinglecastDelegateX<>
MulticastDelegateX<>
MulticastDelegateSafeX<>
SinglecastDelegateX<>
是一个接受单个委托的委托容器。单次转换版本的优点是它稍微小一些,并且允许void
在绑定函数中以外的返回类型。
MulticastDelegateX<>
是一个委托容器,实现为接受多个委托的单链表。只有绑定到具有void
返回类型的函数的委托才能添加到多播委托容器中。
MultcastDelegateSafeX<>
是一个线程安全的容器,实现为接受多个委托的单链表。如果多个线程访问容器实例,请始终使用线程安全版本。
每个容器按值存储委托。这意味着委托被内部复制到堆或固定块内存中,具体取决于模式。在插入容器之前,用户不需要在堆上手动创建委托。通常,重载的模板函数MakeDelegate()
用于根据函数参数创建委托实例。
同步代表
所有委托都是使用重载的MakeDelegate()
模板函数创建的。编译器使用模板参数推导来选择正确的MakeDelegate()
版本,无需手动指定模板参数。例如,这是一个简单的自由函数。
要将自由函数绑定到委托,请DelegateFree1<int>
使用MakeDelegate()
. DelegateFree
模板参数是int
函数参数。MakeDelegate()
返回一个DelegateFree1<int>
对象,下面的行FreeFuncInt
使用委托调用函数。
成员函数以相同的方式绑定到委托,只是这次MakeDelegate()
使用两个参数:类实例和成员函数指针。两个DelegateMember1
模板参数是类名和函数参数。
与创建具体的自由或成员委托不同,通常使用委托容器来保存一个或多个委托。委托容器可以容纳任何委托类型。例如,绑定到具有void (int)
函数签名的任何函数的多播委托容器如下所示。
以相同方式创建单个演员表委托。
float (int)
通过添加额外的模板参数来定义返回值的函数签名,例如。
ASinglecastDelegate<>
可以绑定到返回值的函数,而多播版本不能。原因是当调用多个回调时,应该使用哪个回调函数返回值?正确答案是否定的,因此多播容器只接受带有函数签名void
的委托作为返回类型。
更多的函数参数意味着使用MulticastDelegate2
orMulticastDelegate3
版本。目前,该库最多支持五个函数参数。