为什么信号和插槽比普通的旧回调更好

Why are signals and slots better than plain old callbacks?

本文关键字:回调 更好 信号 插槽 为什么      更新时间:2023-10-16

这里是C++新手。我在读《信号和插槽的更深入研究》,该书声称1)回调本质上是类型不安全的,2)为了使它们安全,你需要在函数周围定义一个纯虚拟类包装器。我很难理解为什么这是真的。例如,Qt在其信号和插槽的教程页面上提供了以下代码:

// Header file
#include <QObject>
class Counter : public QObject
{
    Q_OBJECT
public:
    Counter() { m_value = 0; }
    int value() const { return m_value; }
public slots:
    void setValue(int value);
signals:
    void valueChanged(int newValue);
private:
    int m_value;
};
// .cpp file
void Counter::setValue(int value)
{
    if (value != m_value) {
        m_value = value;
        emit valueChanged(value);
    }
}
// Later on...
Counter a, b;
QObject::connect(&a, SIGNAL(valueChanged(int)),
                 &b, SLOT(setValue(int)));
a.setValue(12);     // a.value() == 12, b.value() == 12
b.setValue(48);     // a.value() == 12, b.value() == 48

以下是使用回调重写的代码:

#include <functional>
#include <vector>
class Counter
{
public:
    Counter() { m_value = 0; }
    int value() const { return m_value; }
    std::vector<std::function<void(int)>> valueChanged;
    void setValue(int value);
private:
    int m_value;
};
void Counter::setValue(int value)
{
    if (value != m_value) {
        m_value = value;
        for (auto func : valueChanged) {
            func(value);
        }
    }
}
// Later on...
Counter a, b;
auto lambda = [&](int value) { b.setValue(value); };
a.valueChanged.push_back(lambda);
a.setValue(12);
b.setValue(48);

正如你所看到的,回调版本是类型安全的,并且比Qt版本更短,尽管他们声称它不是。除了Counter之外,它没有定义任何新的类。它只使用标准库代码,不需要特殊的编译器(moc)即可工作。那么,为什么信号和插槽比回调更受欢迎呢?C++11是否已经简单地抛弃了这些概念?

谢谢。

两者之间有一个巨大的区别:线程。

传统回调总是在调用线程的上下文中调用。信号和插槽则不然——只要线程正在运行一个事件循环(如果是QThread,则会是这样),插槽就可以在任何线程中。

当然,您可以通过回调手动完成所有这些操作——多年来,我编写了许多Win32应用程序,这些应用程序使用Windows风格的消息泵来处理线程间的回调——但这是大量的样板代码,编写、维护或调试没有多大乐趣。

为什么信号和插槽比普通的旧回调更好?

因为信号很像普通的旧回调,除了具有额外的功能并与QtAPI深度集成之外。这不是火箭科学——回调+额外功能+深度集成比单独的回调更重要。C++可能最终提供了一种更干净的回调方式,但这并不能取代Qt信号和插槽,更不用说让它们过时了。

自从Qt 5允许信号连接到任何功能以来,插槽方面变得不那么重要了。但是,slots仍然与Qt元系统集成,许多Qt API都使用该系统来实现工作。

是的,你可以对几乎所有信号应该实现的事情使用回调。但这并不容易,它有点冗长,它不会自动处理排队的连接,它不会像信号那样与Qt集成,你可能也可以解决这个问题,但它会变得更加冗长。

在QML的情况下,它现在是Qt的主要焦点,你基本上被Qt的信号卡住了。所以我认为信号会一直存在。

信号和槽是"更好的",因为Qt在概念上是围绕它们构建的,它们是API的一部分,并且被许多API使用。这些概念在Qt中已经存在了很长一段时间,从那时起,C++除了从C继承的简单的旧函数指针之外,没有提供太多的回调支持。这也是Qt不能简单地切换到std回调的原因——它会破坏很多东西,而且是不必要的工作。Qt继续使用那些邪恶的、不安全的普通旧指针而不是智能指针的原因也是一样的。信号和时隙作为一个概念并没有过时,在使用Qt时,技术上更没有过时。C++在游戏中太晚了。既然C++最终作为语言标准库的一部分提供了替代方案,那么期望每个人现在都会匆忙放弃自己在庞大代码库中的实现是不现实的。

一般来说:信号和插槽与回调的不同之处在于它将调用(Signal)与处理程序(Slot)解耦。这意味着:你可以将你的插槽注册到不同的线程上,你可以从多个插槽中收听一个信号,并轻松更改排队策略。但它也有代价(至少在QT世界…):字符串求值和通常更多的内部工作/代码分支。。简而言之,这是一个更高层次的概念。

话虽如此,你可以通过简单的回调来完成所有这些,但这就像重新发明轮子一样。