C++ At Work 专栏...
事件编程(二)
原著:Paul DiLascia
翻译:NorthTibet
下载源代码:CAtWork0603.exe (3,178KB)
原文出处:Event Programming, Part 2
在本文的第一部分(事件编程一),我回答了一个关于用 C++ 实现本机事件的问题。讨论了一般意义上的事件并示范了如何用接口为你的类定义事件处理器,事件的处理必须在客户机实现。我的实现有一些缺陷,我承诺过最终要解决掉,本文就来完成这件事情。
在开始之前,先简单回顾一下前面写的那个程序,PrimeCalc。如 Figure 1 所示:
Figure 1 计算素数
程序中使用了一个计算素数的类 CPrimeCalculator,这个类发起两个事件:Progress 和 Done。当搜索到素数时,该类触发 Progress 事件以报告目前发现了多少素数。完成处理后触发 Done 事件。这两个事件都是由接口 IPrimeEvents 定义的:
class IPrimeEvents {public: virtual void OnProgress(UINT nPrimes) = 0; virtual void OnDone() = 0;};客户机要想处理事件必须得从 IPrimeEvents 派生,实现事件处理函数,并调用 CPrimeCalculator::Register 来注册其接口。CPrimeCalculator::Register 会将客户机对象/接口添加到其内部列表。当触发了一个 Progress 事件时,CPrimeCalculator 便调用辅助函数 NotifyProgress:
void CPrimeCalculator::NotifyProgress(UINT nFound){ listNotifyProgress 遍历客户机列表,调用每个客户机的 OnProgress 处理函数。当某个程序员使用 CPrimeCalculator 时,编写事件处理代码很容易——只要从 IPrimeEvents 派生并实现处理器即可。但是在实现这种触发事件的 CPrimeCalculator 类机制时冗长乏味。你必须得为每个事件(如 Foo)实现诸如 NotifyFoo 这样的函数,即使处理模式一模一样。事件触发代码被划分在两个类中,事件接口 IPrimeEvents 和 事件源 CPrimeCalculator。如果你想将同样的事件接口用于不同的事件源那该怎么办?IPrimeEvents 是很通用,我可能将它改名为 IProgressEvents 并将它用于任何以整数形式报告处理进度的类并在完成处理时用 Done。但每个触发 Progess 事件的类必须重新实现触发事件的通知函数。理想情况下,所有事件代码都应该放在单个类中。::iterator it; for (it=m_clients.begin(); it!=m_clients.end(); it++) { (*it)->OnProgress(nFound); }}
既然通知函数在本文中是一种实验模型,那么自然会问这样的问题:它们有没有某种通用的实现方法?我能将整个事件机制封装到单个的类、模板或宏,或者任何事件源能使用的其它什么类型中吗?答案是肯定中的肯定。我将示范如何创建一个使用宏和模板的事件系统,以便将事件处理的代码量降至最低限度。我们的旅程需要借助一些高境界的 C++ 操作,比如嵌套模板以及仿函数类(functor class)。
我将分几个步骤实现这个系统。目的是编写一个实现通知函数 NotifyProgress 以及 NotifyDone 的模板。每个函数都具备相似而又不完全一样的模型:
// NotifyFoo — raise Foo eventlist<IPrimeEvents*::iterator it;for (it=m_clients.begin(); it!=m_clients.end(); it++) { (*it)-OnFoo(/*args*/);}也就是说迭代客户机列表,并针对每个客户机调用 OnFoo,传递事件参数。如何把它写成一个模板呢?可以将接口 IPrimeEvents 参数化为一个类型 T,但如何参数化事件处理函数 OnFoo,程序员可能选择的任何名字和签名。
任何时候你参数化某个函数时,都应该考虑:仿函数,也叫做 functor。仿函数是 C++ 语言中将函数转换为类的一种机制,它代替了给回调函数传递指针的做法,而是传递仿函数类的实例。在标准模板库 STL 中包含有丰富的 Functor,并实现了一些使用 functor 的算法,尤其是 for_each 算法,在本文中很有用:
for_each(m_clients.begin(), m_clients.end(),NotifyProgress(nFound));
for_each 算法从头到尾迭代容器元素,并对每个元素调用函数对象 NotifyProgress。这里说的“函数对象”到底是指的什么呢?不是一个函数,它是一个对象。这个类看起来像下面这个样子:
class NotifyProgress {protected: UINT m_nFound;public: NotifyProgress(UINT n) : nFound(n) { } void operator()(IPrimeEvents* obj) { obj->OnProgress(nFound); }};NotifyProgress 实现函数 operator()(IPrimeEvents*),它是 for_each 算法需要的东西。一般来讲,如果你具备一个类型为 T 对象集合,for_each 会需要一个