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

Why are signals and slots better than plain old callbacks?

这里是 C++ 新手。我正在阅读 A Deeper Look at Signals and Slots, which claims that 1) callbacks are inherently type-unsafe, and 2) to make them safe you need to define a pure virtual class wrapper around your function. I'm having a hard time understanding why that's true. As an example, here is the code Qt provides on their tutorial page for signals and slots:

// 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 版本更短,despite them claiming that it's not. It does not define any new classes, aside from Counter. It uses only standard library code and doesn't need a special compiler (moc) 可以工作。那么,为什么信号和槽优先于回调呢? C++11 是否简单地废弃了这些概念?

谢谢。

Why are signals and slots better than plain old callbacks?

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

自 Qt 5 以来,插槽方面的相关性有所降低,它允许将信号连接到任何函数。但是,插槽仍然与 Qt 元系统集成,许多 Qt APIs 使用它来使事情正常运行。

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

在 QML 的情况下,它现在是 Qt 的主要焦点,您基本上只能使用 Qt 的信号。所以我认为信号会一直存在。

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

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

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

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

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

也就是说,您可以通过简单的回调来完成所有这些操作,但这就像重新发明轮子一样。