参考链接

  1. 抄袭还是未抄袭:信号槽连接中的争论?——布尔克哈德·斯图伯特

  2. Qt基础之三十七:是否发生复制?浅谈参数在信号-槽中的传递_qt信号参数 复制-CSDN博客

先说结论

不管是Direct Connections还是Queued Connections,我们都应该使用const &而不是使用值传递。

当使用值传递的时候,将至少发生一次数据拷贝,如果跨线程,则再发生一次。

当使用const &的时候,同线程一半不会拷贝,跨线程的时候将强制发生一次拷贝,因为引用在线程间传递是不安全的,QT会强制执行数据拷贝,将数据序列化到事件队列中。

当使用指针传递的时候,则指拷贝指针。

额外注意:使用自定义类型,需要使用qRegisterMetaType注册元类型,参考 Qt 线程间信号槽传递自定义数据类型 - Larpx的站点|一个什么都记录的小站

最佳实践

  1. 使用共享指针,即QSharedPointer进行传递,这样即使跨线程操作,也仅拷贝指针,减小系统开销。

  2. 使用const &.

  3. 优化程序结构,值传递的时候,仅传递基本类型、小对象。

参数传递细节解析

注:在QT5.15 MSVC 2022 环境下测试,MyData为class。

操作序号

信号参数类型

槽函数参数类型

Direct Connections 拷贝次数

Queue Connections 拷贝次数

1

const MyData &

const MyData &

0

1

2

const MyData &

MyData

1

2

3

MyData

const MyData &

1

2

4

MyData

MyData

2

3

Direct Connections链接情况下:

操作类型1:

我们需要在类的复制构造函数和赋值运算符中设置断点,这样我们的程序只调用 ,断点就不会被命中。这说明这种传参的操作其实不会发生数据拷贝。其实这结果并不意外,因为这是 C++ 中作为 const & 传递参数的基本工作方式。Direct Connection链接方式只不过是一连串同步或直接的 C++ 函数调用。

下面是信号发出时,函数的调用链。

1. emit sendConstRef(c)
2. MainView::sendConstRef          // generated by moc
3. QMetaObject::activate
4. MainView::qt_static_metacall   // generated by moc
5. MainView::receiveConstRef

第 2、3 和 4 步的元对象代码(分别用于对信号参数进行 marshalling、将发射的信号路由到连接的槽以及对槽的参数进行去 marshalling)的编写方式不会发生参数的数据拷贝。这样就只有两个地方可能会出现复制对象的情况:步骤1将对象传递给函数和步骤5槽函数读取对象.

但是这两个地方又受到标准 C++ 行为的约束,由于两个函数都传递的是常量引用,因此不需要复制数据,直接使用地址即可进行访问。同时对象也不存在生命周期问题,因为在信号发出和槽函数执行结束时,对象会在超出作用域之前返回。

操作类型2:

根据操作1中的分析,我们可以很容易地推断出在这种情况下,会发生一次数据拷贝。当qt_static_meta_call在步骤4中调用槽函数时,由于数据对象是按照值传递的,因此会执行一次拷贝操作。

操作类型3:

同样也会发生了一次值传递,在数据对象传递到信号的时候发生的。

操作类型4:

这种情况则会发生两次数据拷贝,第一次发生在数据对象传递到信号的时候,第二次发生在数据对象传递到槽函数的时候。

Queue Connections 链接情况下:

Queue Connections连接其实不过是异步函数调用而已。从概念上讲,路由函数QMetaObject::activate不再直接调用槽,

而是根据槽函数及其参数创建一个命令对象,并将该命令对象插入到事件队列中去。

当轮到命令对象执行的时候,事件循环的调度器便将该命令对象从队列中删除,并通过调用槽函数来去执行它。

当QMetaObject::activate创建命令对象时,它会在命令对象中存储参数对象的副本。因此对于每个信号槽的组合,都会有一个额外的副本。

这也就是为什么当我们跨线程使用信号传递自定义数据类型的时候,需要使用qRegisterMetaType在Qt的元对象系统中进行注册,需要注意的时候,这时候的自定义数据类型,需要具有默认构造函数、复制构造函数和析构函数,不然无法使用qRegisterMetaType注册,如果不进行注册,则会导致槽函数无法执行。

即使在多线程场景中,我们也应该通过const引用将参数传递给信号和槽,以避免不必要的参数复制。