Differences between String-Based and Functor-Based Connections

从 Qt 5.0 开始, Qt 提供了两种不同的 信号槽连接 方式: 基于字符串 连接语法和 基于函数对象 连接语法. 两种语法都有优点和缺点. 下表总结了它们的差异.

String-basedFunctor-based
类型检测时机运行时编译时
是否执行隐式类型转换Y
是否可以将信号连接到 lambda 表达式Y
是否可以将信号连接到参数比信号多的槽函数 (使用默认参数)Y
是否可以将 C++ 函数连接到 QML 函数Y

以下部分详细解释了这些差异, 并演示如何使用每种连接语法特有的功能.

Type Checking and Implicit Type Conversions

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

  1. 只有在程序开始运行后才能检测到连接错误.
  2. 信号和槽之间不能进行隐式转换.
  3. 无法解析类型定义和名称空间.

存在限制 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()));

      // ...

Making Connections to Lambda Expressions

基于函数对象的连接语法可以将信号连接到 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());
      });

      // ...
  }

在这个示例中, 即使 QPushButton::clicked() 和 TextSender::textCompleted() 具有不兼容的参数, lambda 函数也使连接变得简单. 相反, 基于字符串的实现将需要额外的样板代码.

注意: 基于函数对象的连接语法接受指向所有函数的指针, 包括独立函数和常规成员函数. 然而, 为了可读性, 信号应该只连接到槽, lambda 表达式和其他信号.

Connecting C++ Objects to QML Objects

基于字符串的语法可以将 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!"'.

有关让 C++ 对象与 QML 对象交互的其他方法, 参见 Interacting with QML Objects from C++.

Using Default Parameters in Slots to Connect to Signals with Fewer Parameters

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

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

基于函数对象的连接不支持此功能.

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


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

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


  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 函数, 参见 Making Connections to Lambda Expressions.

Selecting Overloaded Signals and Slots

使用基于字符串的语法, 显式指定参数类型. 因此, 所需的重载信号的实例是明确的.

相反, 使用基于函数对象的语法, 必须强制转换重载的信号或槽, 告诉编译器要使用哪个实例.

例如, 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));

参见 qOverload().