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

当信号的参数是对象的常引用,且槽的参数也是对象的常引用,那么这个对象会复制多少次呢?信号和槽的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

  • 4
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值