基于字符串的连接和基于函数的连接之间的差异

从Qt 5.0起,Qt提供了两种不同的C++信号槽连接方式:基于字符串的连接语法(SIGNAL/SLOT将信号/槽转成一个字符串)和基于函数的连接语法。这两种语法各有优缺点。下表总结了它们之间的区别。

基于字符串

基于函数

类型校验的完成在…

运行时

编译时

可以履行隐式类型转换

Y

可以将信号连接到 Lambda 表达式

Y

当使用默认参数时,可以将信号连接到具有比信号更多参数的槽。

Y

可以将 C++ 函数连接到 QML 函数

Y

以下各节详细解释了这些差异,并演示了如何使用每种连接语法独有的特性。

类型校验和隐式类型转换

基于字符串的连接通过在运行时比较字符串来进行类型检查。这种方法有三个限制:

  1. 连接错误只能在程序启动后检测到。
  2. 信号和槽之间不能进行隐式转换。
  3. 不能解析 typedef 和命名空间。

限制2和3存在是因为字符串比较器无法访问 C++ 类型信息,因此它依赖于精确的字符串匹配。

相比之下,基于函数对象的连接由编译器检查。编译器在编译时捕获错误,允许在兼容类型之间进行隐式转换,并识别同一类型的不同名称。

例如,只有基于函数对象的语法可以将携带 int 的信号连接到接受 double 的槽。 QSlider 持有一个 int 值,而 QDoubleSpinBox 持有一个 double 值。以下代码片段展示了如何使它们保持同步:

auto slider = new QSlider(this);
    auto doubleSpinBox = new QDoubleSpinBox(this);
    // OK: The compiler can convert an int into a double
    connect(slider, &QSlider::valueChanged,
            doubleSpinBox, &QDoubleSpinBox::setValue);
    // ERROR: The string table doesn't contain conversion information
    connect(slider, SIGNAL(valueChanged(int)),
            doubleSpinBox, SLOT(setValue(double)));

以下示例说明了名称解析的缺失。QAudioInput::stateChanged() 声明了一个类型为 “QAudio::State” 的参数。因此,基于字符串的连接也必须指定 “QAudio::State”,即使 “State” 已经可见。这个问题不适用于基于函数对象的连接,因为参数类型不是连接的一部分。

auto audioInput = new QAudioInput(QAudioFormat(), this);
    auto widget = new QWidget(this);
    // OK
    connect(audioInput, SIGNAL(stateChanged(QAudio::State)),
            widget, SLOT(show()));
    // ERROR: The strings "State" and "QAudio::State" don't match
    using namespace QAudio;
    connect(audioInput, SIGNAL(stateChanged(State)),
            widget, SLOT(show()));
    // ...

使连接到 Lambda 表达式

基于函数对象的连接语法可以将信号连接到 C++11 lambda 表达式,这些表达式实际上是内联槽。这个特性在基于字符串的语法中是不可用的。

在以下示例中,TextSender 类发出一个携带 QString 参数的 textCompleted() 信号。以下是类的声明:

class TextSender : public QWidget {
    Q_OBJECT
    QLineEdit *lineEdit;
    QPushButton *button;
signals:
    void textCompleted(const QString& text) const;
public:
    TextSender(QWidget *parent = nullptr);
};

当用户点击按钮时,以下是发射 TextSender::textCompleted() 信号的示例:

TextSender::TextSender(QWidget *parent) : QWidget(parent) {
    lineEdit = new QLineEdit(this);
    button = new QPushButton("Send", this);
    connect(button, &QPushButton::clicked, [=] {
        emit textCompleted(lineEdit->text());
    });
    // ...
}

在这个例子中,lambda函数使连接变得简单,即使 QPushButton::clicked() 和 TextSender::textCompleted() 的参数是不兼容的。相比之下,基于字符串的实现需要额外的样板代码。

注意: 对于函数对象连接语法,可以接受所有函数的指针,包括独立函数和普通成员函数。但是,为了提高可读性,建议将信号仅连接到槽函数、Lambda表达式和其他信号。

将 C++ 对象连接到 QML 对象

基于字符串的语法可以将 C++ 对象连接到 QML 对象,但基于函数对象的语法不能。这是因为 QML 类型是在运行时解析的,所以它们不可用于 C++ 编译器。

在以下示例中,点击 QML 对象会使 C++ 对象打印一条消息,反之亦然。以下是 QML 类型(在 QmlGui.qml 文件中)的示例:

Rectangle {
    width: 100; height: 100
    signal qmlSignal(string sentMsg)
    function qmlSlot(receivedMsg) {
        console.log("QML received: " + receivedMsg)
    }
    MouseArea {
        anchors.fill: parent
        onClicked: qmlSignal("Hello from QML!")
    }
}

以下是 C++ 类的代码示例:

class CppGui : public QWidget {
    Q_OBJECT
    QPushButton *button;
signals:
    void cppSignal(const QVariant& sentMsg) const;
public slots:
    void cppSlot(const QString& receivedMsg) const {
        qDebug() << "C++ received:" << receivedMsg;
    }
public:
    CppGui(QWidget *parent = nullptr) : QWidget(parent) {
        button = new QPushButton("Click Me!", this);
        connect(button, &QPushButton::clicked, [=] {
            emit cppSignal("Hello from C++!");
        });
    }
};

以下是建立信号-槽连接的代码示例:

auto cppObj = new CppGui(this);
    auto quickWidget = new QQuickWidget(QUrl("QmlGui.qml"), this);
    auto qmlObj = quickWidget->rootObject();
    // Connect QML signal to C++ slot
    connect(qmlObj, SIGNAL(qmlSignal(QString)),
            cppObj, SLOT(cppSlot(QString)));
    // Connect C++ signal to QML slot
    connect(cppObj, SIGNAL(cppSignal(QVariant)),
            qmlObj, SLOT(qmlSlot(QVariant)));

注意: 在 QML 中,所有 JavaScript 函数都接受 var 类型的参数,它在 C++ 中对应 QVariant 类型。

当 QPushButton 被点击时,控制台会打印出 'QML received: “Hello from C++!”。同样,当矩形被点击时,控制台会打印出 'C++ received: “Hello from QML!”

参见 Interacting with QML Objects from C++,了解让 C++ 对象与 QML 对象交互的其他方法。

使用槽默认参数连接到具有较少参数的信号

通常情况下,只有当槽的参数数量与信号相同(或更少),并且所有参数类型都兼容时,才能建立连接。

基于字符串的连接语法提供了一个解决方案:如果槽具有默认参数,则可以从信号中省略这些参数。当信号发射时参数少于槽时,Qt 将使用默认参数值运行槽。

函数对象连接不支持此功能。

假设有一个名为 DemoWidget 的类,其中有一个带有默认参数的槽 printNumber()

public slots:
    void printNumber(int number = 42) {
        qDebug() << "Lucky number" << number;
    }

使用基于字符串的连接,即使 QApplication::aboutToQuit() 没有参数,也可以将 DemoWidget::printNumber() 连接到它。而基于函数对象的连接会产生编译时错误。

DemoWidget::DemoWidget(QWidget *parent) : QWidget(parent) {
    // OK: printNumber() will be called with a default value of 42
    connect(qApp, SIGNAL(aboutToQuit()),
            this, SLOT(printNumber()));
    // ERROR: Compiler requires compatible arguments
    connect(qApp, &QCoreApplication::aboutToQuit,
            this, &DemoWidget::printNumber);
}

为了解决函数对象语法的这种限制,可以将信号连接到一个调用槽的 lambda 函数。请参见上面的章节(使连接到 Lambda 表达式)。

选择重载的信号和槽

使用基于字符串的语法,参数类型被明确指定。因此,对于重载的信号或槽,所需的实例是明确的。

相比之下,使用函数对象语法,必须对重载的信号或槽进行转换,以告诉编译器使用哪个实例。

例如,QLCDNumber 有三个版本的 display() 槽:

  1. QLCDNumber::display(int)
  2. QLCDNumber::display(double)
  3. QLCDNumber::display(QString)

要将 int 版本连接到 QSlider::valueChanged(),两种语法如下:

auto slider = new QSlider(this);
    auto lcd = new QLCDNumber(this);
    // String-based syntax
    connect(slider, SIGNAL(valueChanged(int)),
            lcd, SLOT(display(int)));
    // Functor-based syntax, first alternative
    connect(slider, &QSlider::valueChanged,
            lcd, static_cast<void (QLCDNumber::*)(int)>(&QLCDNumber::display));
    // Functor-based syntax, second alternative
    void (QLCDNumber::*mySlot)(int) = &QLCDNumber::display;
    connect(slider, &QSlider::valueChanged,
            lcd, mySlot);
    // Functor-based syntax, third alternative
    connect(slider, &QSlider::valueChanged,
            lcd, QOverload<int>::of(&QLCDNumber::display));
    // Functor-based syntax, fourth alternative (requires C++14)
    connect(slider, &QSlider::valueChanged,
            lcd, qOverload<int>(&QLCDNumber::display));