1.14 Observer 之谬
本章§1.8 把shared_ptr/weak_ptr 应用到Observer 模式中,部分解决了其线程安全问题。我用Observer 举例,因为这是一个广为人知的设计模式,但是它有本质的问题。
Observer 模式的本质问题在于其面向对象的设计。换句话说,我认为正是面向对象(OO)本身造成了Observer 的缺点。Observer 是基类,这带来了非常强的耦合,强度仅次于友元(friend)。这种耦合不仅限制了成员函数的名字、参数、返回值,还限制了成员函数所属的类型(必须是Observer 的派生类)。
Observer class 是基类,这意味着如果Foo 想要观察两个类型的事件(比如时钟和温度),需要使用多继承。这还不是最糟糕的,如果要重复观察同一类型的事件(比如1 秒一次的心跳和30 秒一次的自检),就要用到一些伎俩来work around,因为不能从一个Base class 继承两次。
现在的语言一般可以绕过Observer 模式的限制,比如Java 可以用匿名内部类,Java 8 用Closure,C# 用delegate,C++(www.cppentry.com) 用boost::function/ boost::bind 2。
在C++(www.cppentry.com) 里为了替换Observer,可以用Signal/Slots,我指的不是QT 那种靠语言扩展的实现,而是完全靠标准库实现的thread safe、race condition free、threadcontention free 的Signal/Slots,并且不强制要求shared_ptr 来管理对象,也就是说完全解决了§1.8 列出的Observer 遗留问题。这会用到§2.8 介绍的“借shared_ptr实现copy-on-write”技术。
在C++(www.cppentry.com)11 中,借助variadic template,实现最简单(trivial)的一对多回调可谓不费吹灰之力,代码如下。
- recipes/thread/SignalSlotTrivial.h
- template<typename Signature>
- class SignalTrivial;
- // NOT thread safe !!!
- template <typename RET, typename... ARGS>
- class SignalTrivial<RET(ARGS...)>
- {
- public:
- typedef std::function<void (ARGS...)> Functor;
- void connect(Functor&& func)
- {
- functors_.push_back(std::forward<Functor>(func));
- }
- void call(ARGS&&... args)
- {
- for (const Functor& f: functors_)
- {
- f(args...);
- }
- }
- private:
- std::vector<Functor> functors_;
- };
- recipes/thread/SignalSlotTrivial.h
我们不难把以上基本实现扩展为线程安全的Signal/Slots,并且在Slot 析构时自动unregister。有兴趣的读者可仔细阅读完整实现的代码(recipes/thread/SignalSlot.h)。
结语
《C++(www.cppentry.com) 沉思录》(Ruminations on C++(www.cppentry.com) 中文版)的附录是王曦和孟岩对作者夫妇二人的采访,在被问到“请给我们三个你们认为最重要的建议”时,Koenig 和Moo 的第一个建议是“避免使用指针”。我2003 年读到这段时,理解不深,觉得固然使用指针容易造成内存方面的问题,但是完全不用也是做不到的,毕竟C++(www.cppentry.com) 的多态要通过指针或引用来起效。6 年之后重新拾起来,发现大师的观点何其深刻,不免掩卷长叹。
这本书详细地介绍了handle/body idiom,这是编写大型C++(www.cppentry.com) 程序的必备技术,也是实现物理隔离的“法宝”,值得细读。
目前来看,用shared_ptr 来管理资源在国内C++(www.cppentry.com) 界似乎并不是一种主流做法,很多人排斥智能指针,视其为“洪水猛兽”(这或许受了auto_ptr 的垃圾设计的影响)。据我所知,很多C++(www.cppentry.com) 项目还是手动管理内存和资源,因此我觉得有必要把我认为好的做法分享出来,让更多的人尝试并采纳。我觉得shared_ptr 对于编写线程安全的C++(www.cppentry.com) 程序是至关重要的,不然就得“土法炼钢”,自己“重新发明轮子”。这让我想起了2001 年前后STL 刚刚传入国内,大家也是很犹豫,觉得它性能不高,使用不便,还不如自己造的容器类。10 年过去了,现在STL 已经是主流,大家也适应了迭代器、容器、算法、适配器、仿函数这些“新”名词、“新”技术,开始在项目中普遍使用(至少用vector 代替数组嘛)。我希望,几年之后人们回头看本章内容,觉得“怎么讲的都是常识”,那我的写作目的也就达到了。