背景
回调列表是个很常见的东东,经常被用在 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 的函数
- 在需要时,支持批量地收集这些回调函数的返回值
接口
说完了好处,接下来看一下 BtMulticast
这个类的对外接口和基本的使用吧:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
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 的所谓“完美转发”来把任意类型和个数的参数转发给回调函数
- 考虑到 add/remove 通常只发生一次,而每次触发事件都会遍历,内部的存储选择
std::vector
,牺牲了一点 add/remove 时的查找速度,换得更快更紧凑的遍历。而看一下实现代码就可以知道,牺牲的那点 add/remove 速度也只有在有 tag 的情况下会发生。
用法
使用方面,基本用法如下:
1
2
3
4
5
6
7
8
|
// 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();
}
|
三个匿名函数被添加进 test
对象,然后在 test.Invoke()
的时候被依次调用。
1
2
3
4
5
6
7
8
9
|
// 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”)。
1
2
3
4
5
6
7
8
9
10
11
|
// 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");
for (auto& p : testRet.InvokeR(20, 2))
BT_LOG("Multicast (with RetVal): func %s returned %d. ", p.second, p.first);
}
|
最后这个用例测试了多个参数和返回值的情况,可以看到 “a”, “b”, “c” 做了不同的操作后,返回的值被采集到了一个返回值列表里面,这个列表被就地 (即所谓的 move
语意) 遍历,内部的值可以根据需要再进行处理。这个用例的运行结果如下:
1
2
3
4
|
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()
,
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
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.emplace_back(func, tag);
return true;
}
|
当 tag 有效时,先判定是否有 tag 冲突,然后注册一下回调,过程很直白就不多说了。
再看一下具体的调用过程 BtMulticast::InvokeR()
,
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
/* ----- 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)
{
ret.emplace_back(p.first(std::forward<U>(u)...), p.second);
}
return ret;
}
|
这里可以看到我单独定义了一下返回值的类型,具体原因见注释,大体上是说类内定义的类型 TRetVect
只能在类内使用 (包括类定义及相关的成员函数体的定义,成员函数的签名不算在内)。另外这函数前面的两个 template
声明分别是类的模板和函数的模板。
俺一直觉得 C++ 的模板声明挺啰嗦,很有孔乙己范儿,看了上面这个函数声明,你也一定深有同感罢。应该跟 D 学一下,简化一下。
C++ 的 typedef
和 class template
,
1
2
3
4
5
6
|
typedef double A;
template<class T> struct B
{
typedef int A;
};
|
D 的对应语法 alias
和模板的 (T)
语法,简洁到没朋友。
1
2
3
4
5
6
|
alias A = double;
class B(T)
{
alias A = int;
}
|
不过 C++ 已经把 D 的 alias
关键字的用法学来了,翻到前面可以看到 class BtMulticast
的定义中的那一组 using
,把 alias
抄了个十足十,啧啧,借鉴得不错。
BtMulticast
类的实现和测试用例代码见这里。
[2015-07-23] 补:代码中的 .push_back(std::make_pair(func, tag))
已替换为更紧凑的 .emplace_back(func, tag)
。段落标题也为了清晰起见重新做了划分。
[2015-07-24] 补:有同学反映
函数名不够 fasion,至少把 Invoke 调用弄成 operator() 也行吧。
这个原因很简单,就像 C++ 把 C 语言的强制转型的语法 (Foo *)pObject
给扩展成 dynamic_cast<Foo *>(pObject)
一样,仅仅是为了可读性和易搜索性,Invoke
和 InvokeR
不仅给了这两个函数独特的符号,而且也很容易在批量查找时区分。嗯,朴素一点也挺好。
(全文完)