QT核心-元对象的其他妙用 -- 二

信号槽的参数及返回值

承接上篇,我们先还原Test的代码,仍然使用宏的形式定义信号槽,然后给信号槽增加一个参数

#ifndef TEST_H
#define TEST_H

#include <QObject>

class Test : public QObject
{
    Q_OBJECT
public:
    explicit Test(QObject *parent = nullptr);

signals:
    void testSignal(int);

public slots:
    void testSlot(int) {}
};

#endif // TEST_H

查看元代码

// SIGNAL 0
void Test::testSignal(int _t1)
{
    void *_a[] = { nullptr, const_cast<void*>(reinterpret_cast<const void*>(std::addressof(_t1))) };
    QMetaObject::activate(this, &staticMetaObject, 0, _a);
}
void Test::qt_static_metacall(QObject *_o, QMetaObject::Call _c, int _id, void **_a)
{
    if (_c == QMetaObject::InvokeMetaMethod) {
        auto *_t = static_cast<Test *>(_o);
        Q_UNUSED(_t)
        switch (_id) {
        case 0: _t->testSignal((*reinterpret_cast< int(*)>(_a[1]))); break;
        case 1: _t->testSlot((*reinterpret_cast< int(*)>(_a[1]))); break;
        default: ;
        }
    } else if (_c == QMetaObject::IndexOfMethod) {
        int *result = reinterpret_cast<int *>(_a[0]);
        {
            using _t = void (Test::*)(int );
            if (*reinterpret_cast<_t *>(_a[1]) == static_cast<_t>(&Test::testSignal)) {
                *result = 0;
                return;
            }
        }
    }
}

现在我们就用到了之前在无参情况下一直忽略的参数_a,但是无论是信号还是槽,明明只有一个参数,但是_a的下标却是从第二个开始的。这神奇的第一个参数其实就是QT信号槽一直存在但始终不建议使用的返回值,现在我们给信号槽增加一个返回值再看看元代码。

#ifndef TEST_H
#define TEST_H

#include <QObject>

class Test : public QObject
{
    Q_OBJECT
public:
    explicit Test(QObject *parent = nullptr);

signals:
    QString testSignal(int);

public slots:
    QString testSlot(int) { return "aaa"; }
};

#endif // TEST_H
// SIGNAL 0
QString Test::testSignal(int _t1)
{
    QString _t0{};
    void *_a[] = { const_cast<void*>(reinterpret_cast<const void*>(std::addressof(_t0))), const_cast<void*>(reinterpret_cast<const void*>(std::addressof(_t1))) };
    QMetaObject::activate(this, &staticMetaObject, 0, _a);
    return _t0;
}
void Test::qt_static_metacall(QObject *_o, QMetaObject::Call _c, int _id, void **_a)
{
    if (_c == QMetaObject::InvokeMetaMethod) {
        auto *_t = static_cast<Test *>(_o);
        Q_UNUSED(_t)
        switch (_id) {
        case 0: { QString _r = _t->testSignal((*reinterpret_cast< int(*)>(_a[1])));
            if (_a[0]) *reinterpret_cast< QString*>(_a[0]) = std::move(_r); }  break;
        case 1: { QString _r = _t->testSlot((*reinterpret_cast< int(*)>(_a[1])));
            if (_a[0]) *reinterpret_cast< QString*>(_a[0]) = std::move(_r); }  break;
        default: ;
        }
    } else if (_c == QMetaObject::IndexOfMethod) {
        int *result = reinterpret_cast<int *>(_a[0]);
        {
            using _t = QString (Test::*)(int );
            if (*reinterpret_cast<_t *>(_a[1]) == static_cast<_t>(&Test::testSignal)) {
                *result = 0;
                return;
            }
        }
    }
}

QT信号槽是有返回值的,而且我这边文章主要就是告诉大家,这个返回值在极端情况下是可以被使用的。
在我想做但一直没有完成的orm框架里有更详细的用法github或者gitee,感兴趣可以看看。

返回值的利用

直接上结论,当连接信号槽时第五个参数为直接连接(Qt::DirectConnection)、或者同线程自动连接(Qt::AutoConnection)时可以使用返回值。关于第五个参数的具体区别可以看看我之前的文章。下面一一列举各种情况:

  • 同线程Qt::AutoConnection
#include "Test.h"

#include <QtConcurrent>

Test::Test(QObject *parent)
    : QObject{parent}
{
    connect(this, SIGNAL(testSignal(int)), this, SLOT(testSlot(int)));
    qDebug() << emit testSignal(1); // 打印aaa
}
  • 不同线程Qt::AutoConnection
#include "Test.h"

#include <QtConcurrent>

Test::Test(QObject *parent)
    : QObject{parent}
{
    connect(this, SIGNAL(testSignal(int)), this, SLOT(testSlot(int)));
    QtConcurrent::run([this]() {
        qDebug() << emit testSignal(1); // 输出为空
    });
}
  • Qt::DirectConnection
#include "Test.h"

#include <QtConcurrent>

Test::Test(QObject *parent)
    : QObject{parent}
{
    connect(this, SIGNAL(testSignal(int)), this, SLOT(testSlot(int)), Qt::DirectConnection);
    qDebug() << emit testSignal(1); // 输出aaa
    QtConcurrent::run([this]() {
        qDebug() << emit testSignal(1); // 输出aaa
    });
}
  • Qt::QueuedConnection
#include "Test.h"

#include <QtConcurrent>

Test::Test(QObject *parent)
    : QObject{parent}
{
    connect(this, SIGNAL(testSignal(int)), this, SLOT(testSlot(int)), Qt::QueuedConnection);
    qDebug() << emit testSignal(1); // 输出为空
    QtConcurrent::run([this]() {
        qDebug() << emit testSignal(1); // 输出为空
    });
}
  • Qt::BlockingQueuedConnection
#include "Test.h"

#include <QtConcurrent>

Test::Test(QObject *parent)
    : QObject{parent}
{
    connect(this, SIGNAL(testSignal(int)), this, SLOT(testSlot(int)), Qt::BlockingQueuedConnection);
    //    qDebug() << emit testSignal(1); // 不能在同一个线程发送
    QtConcurrent::run([this]() {
        qDebug() << emit testSignal(1); // 输出aaa
    });
}

出现这个现象的原因是直接连接是等待槽函数执行完之后将其返回值赋值给信号的,而队列连接信号发送完之后立即返回了,槽函数会在合适的时候再执行,下面看一下关键代码(注意不同QT版本代码可能不一样,我使用的是QT5.15.2)

template <bool callbacks_enabled>
void doActivate(QObject *sender, int signal_index, void **argv)
{
    QObjectPrivate *sp = QObjectPrivate::get(sender);

    if (sp->blockSig)
        return;

    Q_TRACE_SCOPE(QMetaObject_activate, sender, signal_index);

    if (sp->isDeclarativeSignalConnected(signal_index)
            && QAbstractDeclarativeData::signalEmitted) {
        Q_TRACE_SCOPE(QMetaObject_activate_declarative_signal, sender, signal_index);
        QAbstractDeclarativeData::signalEmitted(sp->declarativeData, sender,
                                                signal_index, argv);
    }

    const QSignalSpyCallbackSet *signal_spy_set = callbacks_enabled ? qt_signal_spy_callback_set.loadAcquire() : nullptr;

    void *empty_argv[] = { nullptr };
    if (!argv)
        argv = empty_argv;

    if (!sp->maybeSignalConnected(signal_index)) {
        // The possible declarative connection is done, and nothing else is connected
        if (callbacks_enabled && signal_spy_set->signal_begin_callback != nullptr)
            signal_spy_set->signal_begin_callback(sender, signal_index, argv);
        if (callbacks_enabled && signal_spy_set->signal_end_callback != nullptr)
            signal_spy_set->signal_end_callback(sender, signal_index);
        return;
    }

    if (callbacks_enabled && signal_spy_set->signal_begin_callback != nullptr)
        signal_spy_set->signal_begin_callback(sender, signal_index, argv);

    bool senderDeleted = false;
    {
    Q_ASSERT(sp->connections.loadAcquire());
    QObjectPrivate::ConnectionDataPointer connections(sp->connections.loadRelaxed());
    QObjectPrivate::SignalVector *signalVector = connections->signalVector.loadRelaxed();

    const QObjectPrivate::ConnectionList *list;
    if (signal_index < signalVector->count())
        list = &signalVector->at(signal_index);
    else
        list = &signalVector->at(-1);

    Qt::HANDLE currentThreadId = QThread::currentThreadId();
    bool inSenderThread = currentThreadId == QObjectPrivate::get(sender)->threadData.loadRelaxed()->threadId.loadRelaxed();

    // We need to check against the highest connection id to ensure that signals added
    // during the signal emission are not emitted in this emission.
    uint highestConnectionId = connections->currentConnectionId.loadRelaxed();
    do {
        QObjectPrivate::Connection *c = list->first.loadRelaxed();
        if (!c)
            continue;

        do {
            QObject * const receiver = c->receiver.loadRelaxed();
            if (!receiver)
                continue;

            QThreadData *td = c->receiverThreadData.loadRelaxed();
            if (!td)
                continue;

            bool receiverInSameThread;
            if (inSenderThread) {
                receiverInSameThread = currentThreadId == td->threadId.loadRelaxed();
            } else {
                // need to lock before reading the threadId, because moveToThread() could interfere
                QMutexLocker lock(signalSlotLock(receiver));
                receiverInSameThread = currentThreadId == td->threadId.loadRelaxed();
            }


            // determine if this connection should be sent immediately or
            // put into the event queue
            if ((c->connectionType == Qt::AutoConnection && !receiverInSameThread)
                || (c->connectionType == Qt::QueuedConnection)) {
                queued_activate(sender, signal_index, c, argv);
                continue;
#if QT_CONFIG(thread)
            } else if (c->connectionType == Qt::BlockingQueuedConnection) {
                if (receiverInSameThread) {
                    qWarning("Qt: Dead lock detected while activating a BlockingQueuedConnection: "
                    "Sender is %s(%p), receiver is %s(%p)",
                    sender->metaObject()->className(), sender,
                    receiver->metaObject()->className(), receiver);
                }
                QSemaphore semaphore;
                {
                    QBasicMutexLocker locker(signalSlotLock(sender));
                    if (!c->receiver.loadAcquire())
                        continue;
                    QMetaCallEvent *ev = c->isSlotObject ?
                        new QMetaCallEvent(c->slotObj, sender, signal_index, argv, &semaphore) :
                        new QMetaCallEvent(c->method_offset, c->method_relative, c->callFunction,
                                           sender, signal_index, argv, &semaphore);
                    QCoreApplication::postEvent(receiver, ev);
                }
                semaphore.acquire();
                continue;
#endif
            }

            QObjectPrivate::Sender senderData(receiverInSameThread ? receiver : nullptr, sender, signal_index);

            if (c->isSlotObject) {
                c->slotObj->ref();

                struct Deleter {
                    void operator()(QtPrivate::QSlotObjectBase *slot) const {
                        if (slot) slot->destroyIfLastRef();
                    }
                };
                const std::unique_ptr<QtPrivate::QSlotObjectBase, Deleter> obj{c->slotObj};

                {
                    Q_TRACE_SCOPE(QMetaObject_activate_slot_functor, obj.get());
                    obj->call(receiver, argv);
                }
            } else if (c->callFunction && c->method_offset <= receiver->metaObject()->methodOffset()) {
                //we compare the vtable to make sure we are not in the destructor of the object.
                const int method_relative = c->method_relative;
                const auto callFunction = c->callFunction;
                const int methodIndex = (Q_HAS_TRACEPOINTS || callbacks_enabled) ? c->method() : 0;
                if (callbacks_enabled && signal_spy_set->slot_begin_callback != nullptr)
                    signal_spy_set->slot_begin_callback(receiver, methodIndex, argv);

                {
                    Q_TRACE_SCOPE(QMetaObject_activate_slot, receiver, methodIndex);
                    callFunction(receiver, QMetaObject::InvokeMetaMethod, method_relative, argv);
                }

                if (callbacks_enabled && signal_spy_set->slot_end_callback != nullptr)
                    signal_spy_set->slot_end_callback(receiver, methodIndex);
            } else {
                const int method = c->method_relative + c->method_offset;

                if (callbacks_enabled && signal_spy_set->slot_begin_callback != nullptr) {
                    signal_spy_set->slot_begin_callback(receiver, method, argv);
                }

                {
                    Q_TRACE_SCOPE(QMetaObject_activate_slot, receiver, method);
                    QMetaObject::metacall(receiver, QMetaObject::InvokeMetaMethod, method, argv);
                }

                if (callbacks_enabled && signal_spy_set->slot_end_callback != nullptr)
                    signal_spy_set->slot_end_callback(receiver, method);
            }
        } while ((c = c->nextConnectionList.loadRelaxed()) != nullptr && c->id <= highestConnectionId);

    } while (list != &signalVector->at(-1) &&
        //start over for all signals;
        ((list = &signalVector->at(-1)), true));

        if (connections->currentConnectionId.loadRelaxed() == 0)
            senderDeleted = true;
    }
    if (!senderDeleted) {
        sp->connections.loadRelaxed()->cleanOrphanedConnections(sender);

        if (callbacks_enabled && signal_spy_set->signal_end_callback != nullptr)
            signal_spy_set->signal_end_callback(sender, signal_index);
    }
}

这个函数也很大,但我们需要看的关键代码却不多,通过搜索Qt::QueuedConnectionQt::BlockingQueuedConnection可以找到这两种连接方式的代码,其中Qt::BlockingQueuedConnection方式调用槽函数的关键代码是发送了一个事件:

QMetaCallEvent *ev = c->isSlotObject ?
    new QMetaCallEvent(c->slotObj, sender, signal_index, argv, &semaphore) :
    new QMetaCallEvent(c->method_offset, c->method_relative, c->callFunction,
                                      sender, signal_index, argv, &semaphore);
QCoreApplication::postEvent(receiver, ev);

上面代码有一个关键参数argv,这个参数是当前函数doActivate的入参,同时也是发送信号的元代码里的原始参数,所以槽函数里面修改的数据可以直接作用到信号发送方。

Qt::QueuedConnection则进入到了一个新函数:

static void queued_activate(QObject *sender, int signal, QObjectPrivate::Connection *c, void **argv)
{
    const int *argumentTypes = c->argumentTypes.loadRelaxed();
    if (!argumentTypes) {
        QMetaMethod m = QMetaObjectPrivate::signal(sender->metaObject(), signal);
        argumentTypes = queuedConnectionTypes(m.parameterTypes());
        if (!argumentTypes) // cannot queue arguments
            argumentTypes = &DIRECT_CONNECTION_ONLY;
        if (!c->argumentTypes.testAndSetOrdered(nullptr, argumentTypes)) {
            if (argumentTypes != &DIRECT_CONNECTION_ONLY)
                delete [] argumentTypes;
            argumentTypes = c->argumentTypes.loadRelaxed();
        }
    }
    if (argumentTypes == &DIRECT_CONNECTION_ONLY) // cannot activate
        return;
    int nargs = 1; // include return type
    while (argumentTypes[nargs-1])
        ++nargs;

    QBasicMutexLocker locker(signalSlotLock(c->receiver.loadRelaxed()));
    if (!c->receiver.loadRelaxed()) {
        // the connection has been disconnected before we got the lock
        return;
    }
    if (c->isSlotObject)
        c->slotObj->ref();
    locker.unlock();

    QMetaCallEvent *ev = c->isSlotObject ?
        new QMetaCallEvent(c->slotObj, sender, signal, nargs) :
        new QMetaCallEvent(c->method_offset, c->method_relative, c->callFunction, sender, signal, nargs);

    void **args = ev->args();
    int *types = ev->types();

    types[0] = 0; // return type
    args[0] = nullptr; // return value

    if (nargs > 1) {
        for (int n = 1; n < nargs; ++n)
            types[n] = argumentTypes[n-1];

        for (int n = 1; n < nargs; ++n)
            args[n] = QMetaType::create(types[n], argv[n]);
    }

    locker.relock();
    if (c->isSlotObject)
        c->slotObj->destroyIfLastRef();
    if (!c->receiver.loadRelaxed()) {
        // the connection has been disconnected while we were unlocked
        locker.unlock();
        delete ev;
        return;
    }

    QCoreApplication::postEvent(c->receiver.loadRelaxed(), ev);
}

同样的,它也是发送了一个事件,但是构造事件时的参数不是原始参数,而是重新分配的。下面看关键代码

QMetaCallEvent *ev = c->isSlotObject ?
        new QMetaCallEvent(c->slotObj, sender, signal, nargs) :
        new QMetaCallEvent(c->method_offset, c->method_relative, c->callFunction, sender, signal, nargs);

void **args = ev->args();
int *types = ev->types();

types[0] = 0; // return type
args[0] = nullptr; // return value

if (nargs > 1) {
    for (int n = 1; n < nargs; ++n)
        types[n] = argumentTypes[n-1];

    for (int n = 1; n < nargs; ++n)
        args[n] = QMetaType::create(types[n], argv[n]);
}

可以看到入参argv并没有传递给事件,而是根据类型重新分配新对象,所以槽函数的返回值无法作用到信号上。

最后直接连接则在这两个if-else之后,它有三个if分支,调用的函数分别是obj->call(receiver, argv);callFunction(receiver, QMetaObject::InvokeMetaMethod, method_relative, argv);QMetaObject::metacall(receiver, QMetaObject::InvokeMetaMethod, method, argv);。但无论是哪个分支最终调用函数所传入的参数都是原始的入参argv,所以槽函数能够影响到信号的返回值。

结语

兜兜转转终于把这篇文章写完了,我的写作能力实在有限,这篇文章其实想写的还是很多的,我想把每一个细节都讲解清楚,但是又不知道该怎么写,所以一直磨啊磨才终于磨完,但就算是磨完了中间也还是省掉了一些不知道该怎么写的东西,但好歹大致意思说清楚了。

后面可能会有新的文章产出,但可能会和这篇文章一样需要很久才能写出来,最后还写得不是特别好,如果正在看这些文章的童鞋有一些建议的话也可以提出来,我会适当采纳,谢谢。

最后再说一句,其实这篇文章之前还应该有一篇前导文章QT IOC容器-元对象系统更详细的讲解元对象的,但是那篇文章想写的东西更多,最后半天也写不出一句话,所以看这篇文章有疑问的话可以先搜索一下其他元对象相关的文章看看。

最后再次感谢您能读到这里。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值