IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

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

    Gu Lu\'s Blog发表于 2015-07-22 13:01:00
    love 0

    背景说明

    回调列表是个很常见的东东,经常被用在 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++ 的 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 许可协议。



沪ICP备19023445号-2号
友情链接