分类: C/C++
2008-04-23 22:04:28
C At Work 专栏...
事件编程(二)
原著:Paul DiLascia
翻译:
下载源代码: (3,178KB)
原文出处:
在本文的第一部分(事件编程一),我回答了一个关于用 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); } }
// NotifyFoo — raise Foo event list也就是说迭代客户机列表,并针对每个客户机调用 OnFoo,传递事件参数。如何把它写成一个模板呢?可以将接口 IPrimeEvents 参数化为一个类型 T,但如何参数化事件处理函数 OnFoo,程序员可能选择的任何名字和签名。::iterator it; for (it=m_clients.begin(); it!=m_clients.end(); it ) { (*it)->OnFoo(/*args*/); }
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 会需要一个实现 operator()(T) 的仿函数(functor)。它调用该集合中 T 对象的这个操作符。所以这里函数 operator 有一个 IPrimeEvents 指针参数并返回 void —— 因为客户机列表是一个 IPrimeEvents 指针列表。为了传递附加参数,构造函数将它们保存在数据成员里。NotifyProgress(nFound) 调用构造函数以创建一个用 m_nFound=nFound 初始化的堆栈实例。所以,任何触发 Foo 事件的 Foo 仿函数的一般模式是这样的:
class NotifyFoo { protected: ARG1 m_arg1; // whatever, as many as needed public: NotifyProgress(ARG1 a1, ...) : m_arg1(a1) { } void operator()(IMyEvents* obj) { obj->OnFoo(m_arg1, ...); } };构造函数将事件参数作为数据成员来保存,函数 operator 将它们传递到对象事件处理函数。对于所有仿函数来说,最终结果是——将函数 OnFoo 转换为类 NotifyFoo。这样做为什么会有用呢?因为我能编写一个模板。在我开始做之前,有一件事我必须得提一下。那就是你必须从一个叫 unary_function 的 STL 类派生你的仿函数类:
class NotifyProgress : public unary_function也就是说,NotifyProgress 是一个一元函数,其函数 operator 带一个参数,IPrimeEvents 指针并返回 void。该一元函数使你的仿函数类“可适配”,使你能将它与 STL 适配器,如:not1、bind2nd 等等进行结合。但是即使你从来都没有打算使用适配器,就像我的事件处理例程,unary_function 仍然不失为一个好主意,因为它向这个世界宣告:“这是一个函数类。”它是一种将代码文档化的方式。有关适配器的详细讨论,参见 Effective STL:50 Specific Ways to Improve Your Use of the Standard Template Library (Addison-Wesley, 2001) by Scott Meyers{ . . // as before . };
class NotifyDone : public unary_function现在我有了自己的仿函数类,我可以用 for_each 代替手工迭代客户机列表。可我把它们放在哪呢?仿函数属于与事件说明有关的范畴,所以我把它们放在 IPrimeEvents 接口中,用嵌套类的形式。代码如 Figure 2 所示。细心的读者会注意到我在两个地方还做了细小的恶修改。仿函数的命名没有用 NotifyProgress,而是叫做 Progress。稍后你会明白这样做使代码更易读;还有就是我没有把事件处理器都声明为纯虚拟函数,而是将它们定义为空实现。IPrimeEvents 只有两个事件,但对于一般的事件机制来说,如果程序员感兴趣的的处理并不多,但要让他们实现每一个事件处理器似乎不是很友好。所以这里每个处理器默认实现什么也不做。为了使基类抽象化,我声明了一个纯虚拟析构函数。当你想抽象化一个没有任何纯虚函数的基类时,这是一个标准的C 技巧。唯一的要做的是你必须定义一个析构函数。纯虚拟函数没有定义——除非它是析构函数。既然每一个派生类的析构都调用其基类的析构函数,那么基类需要一个实现,即便它是纯虚拟的:{ public: NotifyDone() { } void operator()(IPrimeEvents* obj) { obj->OnDone(); } };
inline IPrimeEvents::~IPrimeEvents() { }有了我的仿函数定义,CPrimeCalculator 是这样触发 Progress 事件的:
// in CPrimeCalculator: void NotifyProgress(UINT nFound) { for_each(m_clients.begin(), m_clients.end(), IPrimeEvents::Progress(nFound)); }到这里,我已经介绍了仿函数类 Progress 和 Done,同时,NotifyProgress 和 NotifyDone 都能用 STL 的 for_each 算法。下一步该做什么?记住,我的目的是完全摆脱 NotifyFoo 函数——或者说得更具体一点,就是把它们实现为模板,以便程序员在创建事件时不必为他们定义的每个事件编写千篇一律的函数。将 for 循环转化为 for_each 算法只是万里长征的第一步。
template很难相信,平时几乎碰不到的模板套模板的情况?在此处派上用场了。现在触发事件我们可以这样做:class CEventMgr { ... template void Raise(F fn) { for_each(m_clients.begin(), m_clients.end(), fn); } };
void NotifyProgress(UINT nFound) { m_eventmgr.Raise(IPrimeEvents::Progress(nFound)); }
没有 for 循环,甚至都没有 for_each,所有细节都被封装在 CEventMgr
之中,事件的触发使用一行代码。我甚至可以完全省略掉 NotifyProgress,每当想要触发事件时仅仅调用 CEventMgr::Raise
即可——然而,好的编码规范促使我宁愿将 Raise 封装在某个函数中,以防万一我要修改 CEventMgr 或将事件触发函数暴露给客户机。既然 NotifyProgress
是内联函数,就不会有幸能丢失。
如果模板使你伤脑筋,我就再讲清楚一些吧。CEventMgr 是一个参数化的模板类,其参数是事件接口 I。因此 CEventMgr
m_eventmgr.Raise(IPrimeEvents::Progress(nFound));
编译器明白你试图以 IPrimeEvents::Progress 类型参数调用 CEventMgr::Raise,于是它用模板产生成员函数 CEventMgr::Raise(IPrimeEvents::Progress)。实现代码将仿函数实例传递给
for_each,它为客户机列表中的每个 I* 对象调用仿函数的 operator()。仿函数调用对象的 OnProgress
处理例程——这就是我想要的!模板不是很酷吗?
我们已经快到终点了。仿函数让我参数化事件方法并使用 for_each,但它们还是太长,我讨厌敲入太多的东西。所以最后一步是引入一些宏来解决这个问题。下面就是 IPrimeEvents
最终的浓缩定义。
class IPrimeEvents { DECLARE_EVENTS(IPrimeEvents); public: DEFINE_EVENT1(IPrimeEvents, Progress, UINT /*nFound*/); DEFINE_EVENT0(IPrimeEvents, Done); }; IMPLEMENT_EVENTS(IPrimeEvents);完整的源代码参见 Figure 4 —— 从代码中你可以体会到我竭尽全力进行精简和浓缩。只留下最基本的信息:每个事件处理器的名字和签名。宏假设 Foo 的事件处理器是 OnFoo。一些编程的唯美主义者不喜欢宏,但我不那样。有工具为什么不使用呢?DECLARE_EVENTS 声明构造函数和析构函数;IMPLEMENT_EVENTS 实现内联析构。宏 DEFINE_EVENT0,DEFINE_EVENT1 以及 DEFINE_EVENT2 分别声明和定义了 OnFoo 事件处理器以及不带参数,带一个参数和带两个参数的 Foo 事件仿函数。如果你需要更多的参数,可以定义一个结构,用一个事件参数来传递此结构的指针:
MumbleArgs args; args.a = 1; args.b = 2; // etc. m_eventMgr.Raise(IMyEvents::Mumble(&args));
还有一种选择,你可以实现
DEFINE_EVENT3。但是记住:仿函数对象通过值传递的,所以它们应该很小。当可以传递指针时,为什么要在堆区和栈区来回拷贝一大堆参数呢?如果事件处理器需要返回值,也可以借助结构。为了简单起见,我让事件处理器返回
void。
经常有程序员会问仿函数会不会带来太大的额外开销。事实上,仿函数通常比函数更有效率。理由是它是内联的。当你在 C
中传递指向函数的指针时,即使你将函数定义为内联,它就是一个指针。你不能通过传值的方式来传递一个函数。但是当你传递一个对象实例到某个模板函数时,如果你象那样定义函数,编译器产生的所有东西都是内联的。对于事件来说,通过指针仍然只有一个函数调用,它发生在函数
operator 调用虚拟 OnFoo 处理器的时候。
编程愉快!
您的提问和评论可发送到 Paul 的信箱:.