回调列表是个很常见的东东,经常被用在 Observer 这样的 订阅/发布模式 里。当系统触发一个事件时,会遍历所有已经注册的回调列表,挨个调用,通知到相关的对象。
我们知道,为了保持对 C 尽可能的兼容,一直以来,C++ 中的函数并非是所谓的“一级对象” (first-class objects)。而在函数指针的帮助下,我们可以在 C/C++ 中模拟一些 First-class function 才有的特性,比如把函数像值一样以参数传递和保存。到了 C++11 的出现,有了语言和标准库级别的 lambda
/ closure
/ std::function
之后,对函数的操作才变得真正灵活和丰富起来。
常见的 C/C++ 回调列表有以下这几种实现方式:
std::vector<IListener*>
) ,当回调发生时,以虚函数的形式通知到不同的派生类的对象。这个方案的问题在于,凡是想加入这个列表,必须从 IListener 派生,而且所有的虚函数要求签名严格一直,耦合太高,灵活性较差。 std::vector<fnCallback>
) ,当回调发生时,挨个调用容器中的函数指针。这个方案避免了继承的强耦合,但仍需要保证所有的响应函数签名一致,而且每一种类型的响应函数都要定义不同的回调列表,多了之后非常啰嗦,再一个函数指针本身可读性也欠佳。 std::vector<std::function< ... >>
) ,这种回调列表相对于上面两个更加灵活一些,不仅不需要继承,在 std::bind
的帮助下,连函数签名也不需要一致。但问题是,由于 std::function
无法使用 ==
和 !=
来比较(见 参考一(第1条) 和 参考二 ),注销比较麻烦,不像上面两个可以直接指针比较。 那么这里介绍的所谓通用回调列表有何好处呢?
std::function<>
的基础上,可以使用 std::string
作为 tag, 标记那些后面需要被注销的函数,也同时支持不打 tag 的函数 说完了好处,接下来看一下这个类的对外接口和基本的使用吧:
template <typename TRet, class... TArgs> class BtMulticast { public: using TFunc = std::function < TRet(TArgs...) > ; using TElem = std::pair < TFunc, std::string > ; using TRetVect = std::vector < std::pair < TRet, std::string > >; bool AddFunc(TFunc func); bool AddFunc(const std::string& tag, TFunc func); void RemoveFunc(const std::string& tag); template<class... U> void Invoke(U&&... u); template<class... U> TRetVect InvokeR(U&&... u); private: std::vector < TElem > m_funcList; };
这个类很简短, AddFunc
/ RemoveFunc
是添加和删除回调函数, Invoke
/ InvokeR
分别触发无返回值和普通返回值的回调。
需要注意的是,
AddFunc()
可以选择指明 tag, 在这种情况下可通过指明 tag 来 RemoveFunc
InvokeR()
实际上返回的是一个返回值列表,采集了每一个回调的结果 TFunc
这个类型定义了最终存储在 BtMulticast
类中的回调函数对象,利用了 C++11 的所谓“完美转发”来把任意类型和个数的参数转发给回调函数 std::vector
,牺牲了一点 add/remove 时的查找速度,换得更快更紧凑的遍历。而看一下实现代码就可以知道,牺牲的那点 add/remove 速度也只有在有 tag 的情况下会发生。 使用方面,基本用法如下:
// testing multicast: simplest { BtMulticast<void> test; test.AddFunc([]() { BT_LOG("Multicast (simplest): func 1 called. "); }); test.AddFunc([]() { BT_LOG("Multicast (simplest): func 2 called. "); }); test.AddFunc([]() { BT_LOG("Multicast (simplest): func 3 called. "); }); test.Invoke(); }
三个匿名函数被添加进 BtMulticast
对象 (Multicast 是 Multiplex Broadcast 的缩写),然后在 test.Invoke()
的时候被依次调用。
// testing multicast: tagged & single parameter { BtMulticast<void, int> test; test.AddFunc("a", [](int p) { BT_LOG("Multicast (tagged): func a called (param: %d). ", p); }); test.AddFunc("b", [](int p) { BT_LOG("Multicast (tagged): func b called (param: %d). ", p); }); test.AddFunc("c", [](int p) { BT_LOG("Multicast (tagged): func c called (param: %d). ", p); }); test.RemoveFunc("b"); test.Invoke(15); }
三个 tag 分别为 "a", "b", "c" 的匿名函数 (参数为 int
,注意实例化 BtMulticast
时的类型参数列表变化) 被注册进来,然后 tag 为 "b" 的匿名函数被移除,最后以 15 作为参数依次调用剩下的回调函数 ("a" 和 "c")。
// testing multicast with multiple parameters and return value list { BtMulticast<int, int, int> testRet; testRet.AddFunc("a", [](int p1, int p2) -> int { BT_LOG("Multicast (with RetVal): func a called (p1: %d, p2: %d). ", p1, p2); return p1 + 1 * p2; }); testRet.AddFunc("b", [](int p1, int p2) -> int { BT_LOG("Multicast (with RetVal): func b called (p1: %d, p2: %d). ", p1, p2); return p1 + 2 * p2; }); testRet.AddFunc("c", [](int p1, int p2) -> int { BT_LOG("Multicast (with RetVal): func c called (p1: %d, p2: %d). ", p1, p2); return p1 + 3 * p2; }); testRet.RemoveFunc("b"); auto r = testRet.InvokeR(20, 2); for (auto& p : r) BT_LOG("Multicast (with RetVal): func %s returned %d. ", p.second, p.first); }
最后这个用例测试了多个参数和返回值的情况,可以看到 "a", "b", "c" 做了不同的操作后,返回的值被采集到了 auto r
里面,然后我们遍历这个列表,取出返回值。这个用例的运行结果如下:
Multicast (with RetVal): func a called (p1: 20, p2: 2). Multicast (with RetVal): func c called (p1: 20, p2: 2). Multicast (with RetVal): func a returned 22. Multicast (with RetVal): func c returned 26.
可以看到 BtMulticast
能够适配任意个数和类型的参数,因此可认为具有一定的通用性。
最后我们简单看一下实现。先看看 BtMulticast::AddFunc()
,
template <typename TRet, class... TArgs> bool BtMulticast<TRet, TArgs...>::AddFunc(const std::string& tag, TFunc func) { // check if this tag has been used if (tag.size()) { auto it = std::find_if(m_funcList.begin(), m_funcList.end(), [&tag](const TElem& elem) { return elem.second == tag; }); if (it != m_funcList.end()) return false; } m_funcList.push_back(std::make_pair(func, tag)); return true; }
当 tag 有效时,先判定是否有 tag 冲突,然后注册一下回调,过程很直白就不多说了。
再看一下具体的调用过程 BtMulticast::InvokeR()
,
/* ----- Note ----- `BtMulticastRetVect` is an extra alias especially for the returning type for the signature of InvokeR() below, since `TRetVect` defined inside `BtMulticast` cannot be used in the signature (outside the function body) although `BtMulticastRetVect` is defined separately, it literally equals to `typename BtMulticast::TRetVect` */ template <typename TRet> using BtMulticastRetVect = std::vector < std::pair < TRet, std::string > > ; template <typename TRet, class... TArgs> template <class... U> BtMulticastRetVect<TRet> BtMulticast<TRet, TArgs...>::InvokeR(U&&... u) { BtMulticastRetVect<TRet> ret; for (auto& p : m_funcList) { TRet retSingle = p.first(std::forward<U>(u)...); ret.push_back(std::make_pair(retSingle, p.second)); } return ret; }
这里可以看到我单独定义了一下返回值的类型,具体原因见注释,大体上是说类内定义的类型 TRetVect
只能在类内使用 (包括类定义及相关的成员函数体的定义,成员函数的签名不算在内)。另外这函数前面的两个 template
声明分别是类的模板和函数的模板。
俺一直觉得 C++ 的模板声明挺啰嗦,很有孔乙己范儿,看了上面这个函数声明,你也一定深有同感罢。应该跟 D 学一下,简化一下。
C++ 的 typedef
和 class template
,
typedef double A; template<class T> struct B { typedef int A; };
D 的对应语法 alias
和模板的 (T)
语法,简洁到没朋友。
alias A = double; class B(T) { alias A = int; }
不过 C++ 已经把 D 的 alias
关键字的用法学来了,翻到前面可以看到 class BtMulticast
的定义中的那一组 using
,把 alias
抄了个十足十,啧啧,借鉴得不错。
BtMulticast
类的实现和测试用例代码见 这里 。
[完]
Gu Lu
[2015-07-22]
另:本文遵循 Creative Commons BY-NC-ND 4.0 许可协议。