zoukankan      html  css  js  c++  java
  • 使用 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--
    ", header);
                Log(args...);
            }
    
            void Log(int value)
            {
                printf("--%d--
    ", 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,重点部分下篇文章再说。

  • 相关阅读:
    【luogu P1307 数字反转】 题解
    【luogu P1111 公路修建】 题解
    字符串与正则运算
    Java 正则表达式的总结和一些小例子
    js -history.back(-1)和history.go(-1) 区别
    js
    html _ 提取html片段内的纯文本
    vue-x action 的相互调用
    java通过过滤器 设置跨域允许
    git-搭建企业git服务器
  • 原文地址:https://www.cnblogs.com/ForEmail5/p/7136687.html
Copyright © 2011-2022 走看看