使用 C++11 编写类似 QT 的信号槽——上篇

  了解 QT 的应该知道,QT 有一个信号槽 Singla-Slot 这样的东西。信号槽是 QT 的核心机制,用来替代函数指针,将不相关的对象绑定在一起,实现对象间的通信。

  考虑为 Simple2D 添加一个类似的信号槽,实现对象间的通信。当然,功能比较简单,不过对于 Simple2D 就足够了。最终的使用看起来像是这样的:

class A
{
public:
    void FuncA(int v1, float v2, std::string str)
    {
        log("A: --%d--%f--%s--", v1, v2, str.c_str());
    }
};

class B
{
public:
    void FuncB(int v1, float v2, std::string str)
    {
        log("B: --%d--%f--%s--", v1, v2, str.c_str());
    }
};

 

    A objA;
    B objB;

    Signal<void(int, float, std::string)> signal;

    Slot slot1 = signal.connect(&objA, &A::FuncA);
    Slot slot2 = signal.connect(&objB, &B::FuncB);

    signal(10, 20, "Signal-Slot test");

  类 A 和 类 B 分别有一个函数(返回类型、参数个数及参数类型一样),然后将 A 对象 objA 的 FuncA 函数和 B 对象 objB 的 FuncB 函数绑定到信号对象 signal 中,通过信号 signal 的调用,实现对 FuncA 和 FuncB 函数的调用。输出窗口的输出内容为:

 

  

  Signal-Slot 能够实现对象间的解耦,接下来按照上面的代码,用 C++11 的特性编写信号槽。

 

  信号槽 Signal-Slot

  要实现上面的功能似乎并不困难,核心内容就是对回调函数的使用。

  将需要绑定的对象函数保存到 std::function 中,再把 std::function 保存到信号 Signal 对象中,使用数组保存 std::function 能够实现一个 Signal 对应多个 Slot,最后重载 Signal 的操作符 ()。接下来将围绕上面的步骤实现 Signal-Slot。

 

  std::function

  std::function(引入头文件 <functional>) 是 C++11 的内容,通过 std::function 对 C++ 中各种可调用实体(普通函数、类成员函数、Lambda表达式、函数指针、以及其它函数对象等)的封装,形成一个新的可调用的 std::function 对象。

  如果要将成员函数绑定到 std::function 对象中,可以通过以下的代码实现:

class A
{
public:
    void FuncA(int v1, float v2, std::string str)
    {
        log("A: --%d--%f--%s--", v1, v2, str.c_str());
    }
};

  

    std::function<void(int, float, std::string)> Functional;

    A objA;
    Functional = std::bind(&A::FuncA, objA, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3);

    Functional(20, 55, "functional test");

  输出结果:

  通过 std::bind 函数类成员函数绑定到 std::function 中,但对于参数要使用占位符 std::placeholders::_x,由于 FuncA 函数有 3 个参数,所以要使用 3 个占位符。

  要实现 Signal-Slot,就要把任意的类成员函数绑定到 std::function 中。对于上面的情况,由于 FuncA 函数有 3 个参数,所以要使用 3 个占位符。对于那些不确定参数个数的类成员函数,如何把它们统一的绑定到 std::function 中呢?或许可以把参数个数为 1 - 10 的常用情况都列举出来,但这样并不是一个号方法。

 

  类成员函数的函数指针

  在解决将不确定参数个数的类成员函数绑定到 std::function 前,先看一看不用 std::function 实现的类成员函数的回调函数。

class A
{
public:
    void FuncA(int v1, float v2, std::string str)
    {
        log("A: --%d--%f--%s--", v1, v2, str.c_str());
    }
};

 

    typedef void(A::*Functionl)(int, float, std::string);

    A objA;

    Functionl functional = &A::FuncA;
    A* objAPtr = &objA;

    (objAPtr->*functional)(20, 55, "functional test");

  输出结果:

  实现的方法和普通函数的函数指针类似,只不过定义函数指针的时候要使用类名 + ::,使用的时候也需要使用对象的指针(这意味着你要多保存一个对象指针)。在不使用 std::function 的情况下,实现类成员函数的回调函数要复杂的多。但有一个好处,就是绑定时和函数参数的个数无关。

  结合上面两种方式的类成员函数的回调,就可以解决那个问题了——将不确定参数个数的类成员函数绑定到 std::function。

 

  bind_member 类成员绑定函数

  你应该要注意到,无论是 std:: function<void(int, float, std::string)> 的方式,还是 typedef void(Class::*Functional)(int, float, std::string) 的方式,都必须确定函数的返回类型和参数的类型(一旦 std::function 的函数格式确定了,就不能绑定其他格式的函数)。

  下面要编写一个函数 bind_member,功能是将类成员函数(任意返回类型,任意参数类型,任意参数个数)绑定到 std::function 中。它看上去是这样的:

    std::function<void(int, float, std::string)> Functional;

    A objA;
    Functional = bind_member(&objA, &A::FuncA);

    Functional(20, 55, "functional test");

  输出结果:

  上面使用 bind_member 函数的代码中,你可以看出两种方式实现类成员函数回调的影子。那么如何实现 bind_member 呢?由于存在函数返回类型,所以要用到函数模板;由于函数的参数个数和参数类型不同,所以要用到可变参模板;如果你不了解可变参模板,可以看下面关于可变参模板的简单介绍。

 

 

  可变参模板

  变参模板是 C++11 的新特性,其基本语法为:

template<class... Args>

  和普通模板不同,添加了三个点...,表示 Args 是模板参数包(template type parameter pack),是一连串任意的参数打成的一个包。下面举一个例子(定义一个函数,接受任意参数并输出)说明如何使用可变参模板:

        template<class... Args>
        void Log(Args... args)
        {
            printf("");
        }

  调用函数 Log 时,传入 1、2、3、4 四个参数:

Log(1, 2, 3, 4);

  虽然定义了一个可变参模板的函数 Log,但内部如何实现才能输出 1, 2, 3, 4  呢?也就是如何获取参数包中的参数,如果能分别获取参数包中的参数就能使用函数 printf 输出了。

  这个是参数包的展开问题,可以使用递归函数的方法展开参数包。因此,需要两个重载函数实现参数包的展开:

        template<class T, class... Args>
        void Log(T header, Args... args)
        {
            printf("--%d--\n", header);
            Log(args...);
        }

        void Log(int value)
        {
            printf("--%d--\n", value);
        }

  第二个函数可以理解,但是第一个函数是什么意思?这个先不理它,看下面的函数调用:

        Log(1);                // 1
        Log(1, 2);             // 2
        Log(1, 2, 3);          // 3
        Log(1, 2, 3, 4);       // 4

   1、当传入的参数只有 1 时,毫无疑问会调用第二个函数,将 1 输出。

  2、当传入的参数为 1 和 2 时,可以猜测它会调用第一个函数:1 给 header 变量,然后输出。剩下的 2 给 args,由于 args 只有一个参数 2,所以接下来的 Log(args...) 会调用第二个函数输出 2。参数包的展开结束。

  3、当传入的参数为 1, 2, 3 时,显然它会调用第一个函数:1 给 header 变量,然后输出。剩下的 2, 3 给 args,那么接下来的 Log(args...) 调用的是哪一个函数呢?(第一感觉是 args... 表示着一个变量,应该调用第二个函数才对,因为第二个函数接收一个参数,但这样就不能展开接下来的 2 和 3 了)如果能理解这一步,就能理解如何展开参数包了。答案是 args... 会被拆成两部分,第一个参数 2 为一部分,剩下的 3 作为另一部分。既然分成了两部分,它会调用第一个函数处理(第一次接触变参模板的人,很容易把第一个函数 Log 理解成结束两个参数的函数,但并不是)。 接下来的展开和步骤 2 的只有参数 1 和 2 时一样,所以递归展开参数包结束。

  4、当传入的参数为 1, 2, 3, 4 时,这次用图片来说明:

 

  bind_member 实现

  结合以上的内容,你可以实现 bind_member 函数:

    template<class Return, class Type, class... Args>
    std::function<Return(Args...)> bind_member(Type* instance, Return(Type::*method)(Args...))
    {
        /* 匿名函数 */
        return[=] (Args&&... args) -> Return
        {
            /* 完美转发:能过将参数按原来的类型转发到另一个函数中 */
            /* 通过完美转发将参数传递给被调用的函数 */
            return (instance->*method)(std::forward<Args>(args)...);
        };
    }

 

  代码中只是利用了可变参模板的参数包,解决了函数参数类型和参数个数不确定的问题。然后将函数指针的调用封装在一个匿名函数中,再绑定到 std::function 中。其中使用了 C++11 的完美转发,上面也做了简单的介绍。

 

  避免文章过长,分成两部分来实现 Signal-Slot,重点部分下篇文章再说。

posted @ 2017-07-11 14:29  为了邮箱5  阅读(2602)  评论(1编辑  收藏  举报