使用 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(, , "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(, , "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)(, , "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(, , "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(, , , );
虽然定义了一个可变参模板的函数 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(); //
Log(, ); //
Log(, , ); //
Log(, , , ); //
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,重点部分下篇文章再说。
使用 C++11 编写类似 QT 的信号槽——上篇的更多相关文章
- 使用 C++11 编写类似 QT 的信号槽——下篇
要实现 Signal-Slot,Signal 类中应该拥有一个保存 std::function 的数组: template<class FuncType> class Signal { p ...
- C++11实现Qt的信号槽机制
概述 Qt的信号槽机制是Qt的核心机制,按钮点击的响应.线程间通信等都是通过信号槽来实现的,boost里也有信号槽,但和Qt提供的使用接口很不一样,本文主要是用C++11来实现一个简单的信号槽,该信号 ...
- Qt学习记录--02 Qt的信号槽机制介绍(含Qt5与Qt4的差异对比)
一 闲谈: 熟悉Window下编程的小伙伴们,对其消息机制并不陌生, 话说:一切皆消息.它可以很方便实现不同窗体之间的通信,然而MFC库将很多底层的消息都屏蔽了,尽管使用户更加方便.简易地处理消息,但 ...
- 非Qt工程使用Qt的信号槽机制
非Qt工程,使用Qt的信号槽机制,蛋疼不?反正我现在就是要做这样一件蛋疼的事. 要使用Qt的信号槽机制,下面是从Qt Assist里面关于 signal & slots 的一句介绍: All ...
- VJGUI消息设计-兼谈MFC、QT和信号/槽机制
星期六下午4点,还在公司加班.终于写完了下周要交工的一个程序. 郁闷,今天这几个小时写了有上千行代码吧?虽然大部分都是Ctrl-C+Ctrl-V,但还是郁闷. 作为一个有10年经验的MFC程序员,郁闷 ...
- Qt自定义信号槽的使用浅析+实例
1. Qt中自定义信号槽的使用 Qt框架提供的信号槽在某些特定场景下是无法满足我们的项目需求的,因此我们还设计自己需要的的信号和槽,使用connect()对自定义的信号槽进行连接. 如果想要使用自定义 ...
- Qt Connect 信号 槽
信号和槽机制是 QT 的核心机制 .信号和槽是一种高级接口,应用于对象之间的通信,它是 QT 的核心特性,也是 QT 区别于其它工具包的重要地方.信号和槽是 QT 自行定义的一种通信机制,它独立于标准 ...
- 在非UI线程中更改UI(Delphi使用隐藏窗口来处理,QT使用信号槽)
在Delphi里我记得是使用TThread.Synchronize(TThreadMethod),原理是利用了一个隐藏窗口来处理. 在QT Debug模式一下,碰到了同样的问题,显示错误: canno ...
- Qt的信号槽,一个老MFC的经验
最近在利用闲暇时间研究Qt,大概有3周了,看过了官网的white paper并浏览了一遍<C++ GUI Programming with Qt 4, 2nd Edition>.总的来说, ...
随机推荐
- FastAdmin 教程草稿大纲
FastAdmin 教程草稿大纲 计划 FastAdmin 教程大纲 FastAdmin 环境搭建 phpStudy 2018 安装 一键 CRUD 教程 环境变量配置 环境安装 命令行安装 列出所需 ...
- 单变量微积分笔记20——三角替换1(sin和cos)
sin和cos的常用公式 基本公式: 半角公式: 微分公式: 积分公式: 三角替换 示例1 根据微分公式,cosxdx = dsinx 示例2 示例3 半角公式 示例1 示例2 解法1: 解法2: 综 ...
- 【appium】根据xpath定位元素
1. 背景 本文尝试使用的试验对象是SDK自带的NotePad应用实例,假设已经有两个Notes分别是“note1”和“note2”添加到Notepad上面,我们要做的就是尝试用xpath的方法来定位 ...
- github 改位置
在设置里改位置后,先在本地库上右键"stop track this repo". 然后在在线库重新CLONE.
- 通过shell进行数学计算
对于基本运算,可以使用let, $(())和$[] 对于高级运算,使用expr和bc这两个工具 [hupeng@hupeng-vm shell]$n1= [hupeng@hupeng-vm shell ...
- 文字和img、input并排无法对齐的问题
文字和img.input并排在一行的时候,img和input位置总是会偏移一点,input就用复选框和按钮举例,如下图: 只要在img和input样式加上vertical-align:middle;就 ...
- CORS跨域的概念与TP5的解决方案
namespace app\api\behavior; use think\Response; class CORS{ public function appInit(&$params) { ...
- Chrome 鼠标左键-新标签打开
改chrome设置 1.打开google搜索主页2.打开右下角Settings选项->Search Settings3.找到where results open选项4.把Open each se ...
- 一个PHPer的规划
前言:学PHP过时了吗?PHP开发人员如何快速成长?怎么进行职业规划?特别是近几年非常火热的人工智能,机器学习,区块链技术等等,这多少会带动一些人盲目跟风,迷茫等,下面是PHP大牛魏永强带来的一篇根据 ...
- express处理跨域问题,中间件 CORS
CORS是一个W3C标准,全称是"跨域资源共享"(Cross-origin resource sharing). 1.不用中间件的话可以这样写: app.all('*', func ...