当信号的参数是对象的常引用,且槽的参数也是对象的常引用,那么这个对象会复制多少次呢?信号和槽的direct和queued连接方式有何不同?如果信号和槽的参数都按值传递,会发生什么变化呢?
这个问题通常会在项目的某个时刻(比如说性能优化时)提及,但Qt文档对此只字未提。关于stackoverflow有一个很好的讨论,不过需要读者从所有评论中选择正确的答案。因此,下面让我们系统地讨论参数是如何在信号和槽间传递的。
一.环境搭建
假设有一个Copy类,这个类将在信号和槽之间按引用或按值传递。
copy.h
#ifndef COPY_H
#define COPY_H
#include <QString>
class Copy
{
public:
Copy();
Copy(int id, const QString &name);
Copy(const Copy &rhs);
Copy &operator=(const Copy &rhs);
private:
int m_id;
QString m_name;
};
#endif // COPY_H
copy.cpp
#include "copy.h"
#include <QDebug>
Copy::Copy()
{
}
Copy::Copy(int id, const QString &name)
{
m_id = id;
m_name = name;
qDebug() << "consturctor";
}
Copy::Copy(const Copy &rhs)
{
m_id = rhs.m_id;
m_name = rhs.m_name;
qDebug() << "copy constructor";
}
Copy &Copy::operator=(const Copy &rhs)
{
if (this == &rhs)
{
return *this;
}
m_id = rhs.m_id;
m_name = rhs.m_name;
qDebug() << "assignment operator";
return *this;
}
我们需要另一个类Widget,派生自QObject:
widget.h
#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
#include "copy.h"
QT_BEGIN_NAMESPACE
namespace Ui { class Widget; }
QT_END_NAMESPACE
class Widget : public QWidget
{
Q_OBJECT
public:
Widget(QWidget *parent = nullptr);
~Widget();
private:
Ui::Widget *ui;
Copy m_c;
signals:
void sendConstRef(const Copy &c);
void sendValue(Copy c);
public slots:
void receiveConstRef(const Copy &c);
void receiveValue(Copy c);
void on_pushButton_clicked();
};
#endif // WIDGET_H
widget.cpp
#include "widget.h"
#include "ui_widget.h"
#include <QDebug>
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
, m_c(1, "CaoShangPa")
{
ui->setupUi(this);
Qt::ConnectionType ctype = Qt::DirectConnection;
// qRegisterMetaType<Copy>("Copy");
// Qt::ConnectionType ctype = Qt::QueuedConnection;
connect(this, SIGNAL(sendConstRef(Copy)),
this, SLOT(receiveConstRef(Copy)), ctype);
connect(this, SIGNAL(sendValue(Copy)),
this, SLOT(receiveConstRef(Copy)), ctype);
connect(this, SIGNAL(sendConstRef(Copy)),
this, SLOT(receiveValue(Copy)), ctype);
connect(this, SIGNAL(sendValue(Copy)),
this, SLOT(receiveValue(Copy)), ctype);
}
Widget::~Widget()
{
delete ui;
}
void Widget::receiveConstRef(const Copy &c)
{
}
void Widget::receiveValue(Copy c)
{
}
void Widget::on_pushButton_clicked()
{
emit sendConstRef(m_c);
}
Qt::ConnectionType ctype = Qt::DirectConnection;
// qRegisterMetaType<Copy>("Copy");
// Qt::ConnectionType ctype = Qt::QueuedConnection;
connect(this, SIGNAL(sendConstRef(Copy)),
this, SLOT(receiveConstRef(Copy)), ctype);
connect(this, SIGNAL(sendValue(Copy)),
this, SLOT(receiveConstRef(Copy)), ctype);
connect(this, SIGNAL(sendConstRef(Copy)),
this, SLOT(receiveValue(Copy)), ctype);
connect(this, SIGNAL(sendValue(Copy)),
this, SLOT(receiveValue(Copy)), ctype);
上面的代码用于直接(Directed-Connect)方式。对于队列连接(Queue-Connect),可以注释第一行并取消第二和第三行的注释。
最后像下面这样发送信号就行了:
emit sendConstRef(m_c);
// emit sendValue(m_c);
m_c是一个Copy对象。
二.Direct Connections
1.sendConstRef => receiveConstRef
我们在Copy类的复制构造函数和赋值函数中打印调试信息,如果只调用 emit sendConstRef(m_c),调试信息并没有触发,这并没有什么可惊讶的,因为在直接连接方式下的信号-槽在C++中只是一串同步或直接的C++函数调用而已。
尽管如此,查看sendConstRef信号发出时的函数调用过程,仍具有指导意义。
① emit sendConstRef(m_c)
②Widget::sendConstRef // generated by moc
③ QMetaObject::activate
④ Widget::qt_static_metacall // generated by moc
⑤ Widget::receiveConstRef
元-对象(meta-object)代码中的2,3,4是一个信号的中参数依次派发到与信号连接的槽中,在这种情况下,参数复制没有发生。这里没有参数的生命周期问题,因为receiveConstRef函数在sendConstRef的Copy对象结束之前返回。
2.sendConstRef => receiveValue
根据上一节中的详细分析,我们可以很容易地推断出在这种情况会发生一次复制。当qt_static_meta_call在步骤4中调用receiveValue(Copy c)时,Copy对象是按值传递的,因此必须复制。
3.sendValue => receiveConstRef
发生了一次值传递,在Copy对象传参到sendValue的时候。
4.sendValue => receiveValue
这是最糟糕的状况,发生了两次复制,一次是Copy对象传参到sendValue中,另外一次是Copy对象传参到receiveValue中。
三.Queued Connections
队列连接是一次异步函数调用。从概念上讲,路由函数QMetaObject::activate不再直接调用槽,而是根据槽及其参数创建一个命令对象,并将该命令对象插入事件队列。当轮到命令对象时,事件循环的调度器将从队列中删除命令对象,并通过调用slot来执行它。
当QMetaObject::activate创建命令对象时,它会在命令对象中存储copy对象的副本。因此,对于每个信号槽组合,我们都有一个额外的副本。
我们必须使用命令qRegisterMetaType("Copy")在Qt的元对象系统中注册Copy类;以便使QMetaObject::activate的路由工作。任何元类型都需要具有默认构造函数、复制构造函数和析构函数。这就是Copy类有默认构造函数的原因。
Queued Connections不仅适用于信号sender和信号receiver在同一线程中的情况,而且适用于sender和receiver在不同线程中的情形。即使在多线程场景中,我们也应该通过const引用将参数传递给信号和槽,以避免不必要的参数复制。Qt确保参数传递的多线程安全性。
四.结论
从以上结果得出的结论是,不管是Direct Connections还是Queued Connections,我们都应该通过const引用而不是通过值将参数传递给信号和槽。
原文链接:Copied or Not Copied: Arguments in Signal-Slot Connections? – Burkhard Stubert