Qt-connect详解

2022-9-21 更新,参考CSDN-epollet文章
2022-7-29 更新,参考Qt进阶之路-武威的涛娃

函数原型

1
2
3
QObject::connect(const QObject *sender, const char *signal, 
const QObject *receiver, const char *method,
Qt::ConnectionType type = Qt::AutoConnection);

连接的方式

通过函数名

1
2
3
4
...
QLocalSocket socket;
connect(socket, SIGNAL(error()), this, SLOT(handleError()));
...

其中 SIGNAL() 与 SLOT() 为定义的宏,如下。

1
2
#define SLOT(name) "1"#name
#define SIGNAL(name) "2"#name

通过这两个宏,可以把传入的信号与槽的名字转化为字符串,并区别开来。

优点

当存在多个信号或槽重载时,可以很明显的区分调用。

缺点

写法较为复杂,写时注意括号。

通过函数地址

1
connect(socket, &QLocalSocket::connected, this, &MainWindow::handleConnected);

优点

写法简洁。

缺点

遇到重载时,编译器会报错。

1
2
// error!
connect(socket, &QLocalSocket::error, this, &MainWindow::handleError);

通过函数指针

可以解决上述情况

通过static_cast

1
connect(socket, static_cast<void (QLocalSocket::*)(QLocalSocket::LocalSocketError)>(&QLocalSocket::error), this, &MainWindow::handleError);

通过QOverload模板类

1
connect(btn1, QOverload<QLocalSocket::LocalSocketError>::of(&QLocalSocket::error), this, &MainWindow::handleError);

通过qOverload函数模板

1
2
// C++14 support
connect(btn1, qOverload<QLocalSocket::LocalSocketError>(&QLocalSocket::error), this, &MainWindow::handleError);

通过Lambda表达式

1
2
3
4
connect(btn1, SIGNAL(clicked()), this, [=](bool)->void
{
// do something...
})

优点

简单明了,即用即定义,不用在类声明中添加槽函数。

Qt::ConnectionType

ConnectionType 由5个枚举值组成。

  • Qt::AutoConnection = 0
1
2
默认值,如果receiver位于发出信号的线程中,则使用 Qt::DirectConnection;否则,使用 Qt::QueuedConnection。
连接类型在信号发出时确定。
  • Qt::DirectConnection = 1
1
2
槽函数运行于信号发送者所在的线程,效果上就像是直接在信号发送的位置调用了槽函数。
多线程下比较危险,可能会造成崩溃。
  • Qt::QueuedConnection = 2
1
2
槽函数在控制回到接收者所在线程的事件循环时被调用,槽函数运行于信号接收者所在线程。发送信号后,槽函数不会立即被调用,等到接收者当前函数执行完,进入事件循环之后,槽函数才会被调用。
多线程下用这个类型。
  • Qt::BlockingQueuedConnection = 3
1
2
3
与 Qt::QueuedConnection 相同,只是信号线程阻塞直到槽返回。
※ 如果接收者与发送者位于同一线程中,则不得使用此连接,否则应用程序将死锁。
多线程间需要同步的场景会用到这个。
  • Qt::UniqueConnection = 4
1
2
当此类型为当某个信号和槽已经连接时,在进行重复连接时就会失败,可避免重复连接。
此 flag 可通过 | 与其他 flag 结合使用。

注意事项

  • 一个信号可以和多个槽相连(槽会一个接一个地被调用,调用顺序与连接顺序一致);
  • 多个信号可以连接一个槽(只要任意一个信号产生,这个槽就会被调用);
  • 一个信号可以连接到另一个信号(当第一个信号发送时,第二个信号则立刻发送)。
  • 当不再使用时建议断开连接(虽然当一个对象delete之后,Qt自动取消所有连接到这个对象上面的槽),一是为了代码规范,二是防止在距离销毁这段时间再次收到信号。

connect的返回值

connect的返回值为QMetaObject::Connection,代表一个连接。

通常情况下不用去处理,一般用于验证连接是否有效及断开连接。

但如果使用signal-functor格式连接,没有对象存在,则需要使用此返回值进行断开。

多种连接方式

除了第一种字符串式信号槽外,Qt还支持更多种连接方式:

元方法式

1
2
3
4
5
6
7
8
9
10
11
12
13
// [1] 字符串式信号槽
QObject::connect(const QObject *sender, const char *signal,
const QObject *receiver, const char *method,
Qt::ConnectionType type = Qt::AutoConnection);

// [2] QMetaMethod式信号槽
static QMetaObject::Connection connect(const QObject *sender, const QMetaMethod &signal,
const QObject *receiver, const QMetaMethod &method,
Qt::ConnectionType type = Qt::AutoConnection);

// [3] 对[1]的重载,非static去掉receiver
inline QMetaObject::Connection connect(const QObject *sender, const char *signal,
const char *member, Qt::ConnectionType type = Qt::AutoConnection) const;

2.的调用方式是将信号槽的字符串转化为了QMetaMethod,一般不会使用;
3.中的调用方法是对1.的重载,非静态成员函数,有this指针,省略掉了receiver参数。

函数指针式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// [4] 连接信号到qobject的成员函数
template <typename Func1, typename Func2>
static inline QMetaObject::Connection connect(const typename QtPrivate::FunctionPointer<Func1>::Object *sender, Func1 signal,
const typename QtPrivate::FunctionPointer<Func2>::Object *receiver, Func2 slot,
Qt::ConnectionType type = Qt::AutoConnection);

// [5] 连接信号到非成员函数
template <typename Func1, typename Func2>
static inline typename std::enable_if<int(QtPrivate::FunctionPointer<Func2>::ArgumentCount) >= 0, QMetaObject::Connection>::type
connect(const typename QtPrivate::FunctionPointer<Func1>::Object *sender, Func1 signal, Func2 slot);

// [6] 连接信号到非成员函数。比[5]多一个context,可以设置连接类型
template <typename Func1, typename Func2>
static inline typename std::enable_if<int(QtPrivate::FunctionPointer<Func2>::ArgumentCount) >= 0 &&
!QtPrivate::FunctionPointer<Func2>::IsPointerToMemberFunction, QMetaObject::Connection>::type
connect(const typename QtPrivate::FunctionPointer<Func1>::Object *sender, Func1 signal, const QObject *context, Func2 slot,
Qt::ConnectionType type = Qt::AutoConnection);

[4]的用法也比较多,类似于

1
2
3
MyClass c;
YourClass y;
connect(&c, &MyClass::fun1, &y, &YourClass::fun2);

且[4]的实现形式相比[1]更快一点,省去了字符串查找的过程,但建立连接后是没有区别的。

  • 在一些需要”运行期反射”的情况下(头文件都没有,只知道有这么个对象,和函数的名字),只能用[1];
  • [5]可以连接信号到任意非成员函数指针上。除了槽函数,普通的函数也可以连接。这种连接不支持设置连接类型,可以看作是单纯的函数调用
  • [6]是对connect[5]的重载,增加了一个context对象代替reveicer对象的作用。这种连接是可以设置连接类型的。

functor式

首先,什么是functor

在C++11之前, Qt通过自己的实现来推导函数指针及其参数,即QtPrivate::FunctionPointer, 用来处理信号-槽的连接。

C++11带来了lambda, 以及std::bindstd::function, std::function本身可以存储lambdastd::bind以及FunctionPointer

这时候Qt已有的[4]、[5]、[6]是可以支持FunctionPointer的,而新出现的lambda以及std::bind是不支持的,

QtPrivate::FunctionPointer推导不出这些类型。所以Qt把这些不支持的新类型(主要是lambdastd::bind)称为functor(文档和源码都这么命名),并增加了[7]和[8]以支持functor

1
2
3
4
5
6
7
8
9
10
//[7] 连接信号到任意functor
template <typename Func1, typename Func2>
static inline typename std::enable_if<QtPrivate::FunctionPointer<Func2>::ArgumentCount == -1, QMetaObject::Connection>::type
connect(const typename QtPrivate::FunctionPointer<Func1>::Object *sender, Func1 signal, Func2 slot);

// [8] 连接信号到任意functor。比[7]多一个context,可以设置连接类型
template <typename Func1, typename Func2>
static inline typename std::enable_if<QtPrivate::FunctionPointer<Func2>::ArgumentCount == -1, QMetaObject::Connection>::type
connect(const typename QtPrivate::FunctionPointer<Func1>::Object *sender, Func1 signal, const QObject *context, Func2 slot,
Qt::ConnectionType type = Qt::AutoConnection);

[7]可以将任意信号连接到lambdastd::bind上;

[8]是对[7]的重载,增加了一个context对象代替reveicer对象的作用。这种连接是可以设置连接类型的。

传值与引用

先直接说结论吧:

  1. Qt为了避免队列连接造成的悬挂引用问题,会拷贝一次参数。跨线程调用槽(且是Qt::QueuedConnection)的话这一次拷贝无法避免。
  2. 能传引用就传引用,避免参数多两次拷贝。