转载

一个可注销的通用多路回调列表 (C++)

背景说明

回调列表是个很常见的东东,经常被用在 Observer 这样的 订阅/发布模式 里。当系统触发一个事件时,会遍历所有已经注册的回调列表,挨个调用,通知到相关的对象。

我们知道,为了保持对 C 尽可能的兼容,一直以来,C++ 中的函数并非是所谓的“一级对象” (first-class objects)。而在函数指针的帮助下,我们可以在 C/C++ 中模拟一些 First-class function 才有的特性,比如把函数像值一样以参数传递和保存。到了 C++11 的出现,有了语言和标准库级别的 lambda / closure / std::function 之后,对函数的操作才变得真正灵活和丰富起来。

常见的 C/C++ 回调列表有以下这几种实现方式:

  1. 基类指针 (形如 std::vector<IListener*> ,当回调发生时,以虚函数的形式通知到不同的派生类的对象。这个方案的问题在于,凡是想加入这个列表,必须从 IListener 派生,而且所有的虚函数要求签名严格一直,耦合太高,灵活性较差。
  2. 函数指针 (形如 std::vector<fnCallback> ,当回调发生时,挨个调用容器中的函数指针。这个方案避免了继承的强耦合,但仍需要保证所有的响应函数签名一致,而且每一种类型的响应函数都要定义不同的回调列表,多了之后非常啰嗦,再一个函数指针本身可读性也欠佳。
  3. 函数对象 (形如 std::vector<std::function< ... >> ,这种回调列表相对于上面两个更加灵活一些,不仅不需要继承,在 std::bind 的帮助下,连函数签名也不需要一致。但问题是,由于 std::function 无法使用 ==!= 来比较(见 参考一(第1条) 和 参考二 ),注销比较麻烦,不像上面两个可以直接指针比较。

关注点,接口和用例

那么这里介绍的所谓通用回调列表有何好处呢?

  1. (以所谓“完美转发”的形式)支持任意个数和类型的参数调用
  2. 在上面第三点 std::function<> 的基础上,可以使用 std::string 作为 tag, 标记那些后面需要被注销的函数,也同时支持不打 tag 的函数
  3. 在需要时,支持批量地收集这些回调函数的返回值

说完了好处,接下来看一下这个类的对外接口和基本的使用吧:

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 分别触发无返回值和普通返回值的回调。

需要注意的是,

  1. AddFunc() 可以选择指明 tag, 在这种情况下可通过指明 tag 来 RemoveFunc
  2. InvokeR() 实际上返回的是一个返回值列表,采集了每一个回调的结果
  3. TFunc 这个类型定义了最终存储在 BtMulticast 类中的回调函数对象,利用了 C++11 的所谓“完美转发”来把任意类型和个数的参数转发给回调函数
  4. 考虑到 add/remove 通常只发生一次,而每次触发事件都会遍历,内部的存储选择 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++ 的 typedefclass 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 许可协议。

正文到此结束
Loading...