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

12
新手学习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是否已经使这些概念过时了?

谢谢。


4
Qt比C++11早得多。在C++11标准之前,Qt文档、白皮书等所声称的内容必须被理解为“C++作为一种更好的C”的主张。特别是那些愚蠢的主张,比如“回调有两个基本缺陷:首先,它们不是类型安全的。”对于C++来说没有任何意义:要理解它,必须从纯C编程的角度思考。 - Cheers and hth. - Alf
2
不要将“Qt信号和槽的实现”与“信号和槽”这个概念混淆。当某个东西维护回调列表并在事件发生时调用它们时,你就拥有了信号和槽的本质。其他一切都是语法糖、语法和营销。你拥有的是一个很好但简单的信号和槽系统实现。 - GManNickG
3
在当今的C++11及其之后版本中,使用shared_ptr、lambda表达式、闭包和std::function等功能,采用基于标准C++概念的信号和槽实现可能会更好、更简单(减少模板代码、增加灵活性、减少对元对象编译系统(MOC)的依赖)。但在Qt最初的设计时期并非如此。 - user4842163
1
@JamesKo 假设你有一个C风格的GUI API,并且当按钮被按下时,你想要做一些事情。在这种情况下,你需要传递一个函数指针。然而,当按钮被按下并且调用了你的函数指针时,你希望能够访问应用程序中的一些数据。为了在C中通用地实现这个功能,通常需要通过void指针传递它,将其强制转换为void*,然后当调用你的函数指针时,它可能具有类似于void (*event)(void* user_data)的签名。然后,你需要将user_data转换为之前传递的内容(你想要访问的应用程序数据)。 - user4842163
1
@JamesKo 现在在你的特定示例中,你确实有一个接受 int 的签名。但问题是你的 lambda 是闭包,它们访问这些 Counters。为了设计一个通用的 API,在 C 风格的函数指针回调中可以访问这些计数器或其他任何东西,它们通常必须通过 void 指针压缩,或者你必须完全失去通用性,并设计一个仅在触发事件时使用计数器的 API。要么你把你的计数器变成全局变量。希望这样说得清楚! - user4842163
显示剩余6条评论
3个回答

18

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

传统的回调函数总是在调用线程的上下文中被调用。但是信号和槽不同--只要线程正在运行事件循环(如果它是一个QThread的话),槽可以在任何线程中。

当然,你可以使用回调手动完成所有这些操作--我多年来编写了许多使用Windows风格消息泵在线程之间处理回调的Win32应用程序--但是这需要编写大量样板代码,而且不太好编写、维护或调试。


5
为什么信号和槽(signals and slots)比普通回调(callbacks)更好呢?
因为信号和槽类似于普通回调,同时又具有额外的功能,并且与Qt API深度集成。这并不是什么高深的技术 - 回调 + 额外功能 + 深度集成 要优于仅有回调。虽然C++现在提供了一种更清晰的回调方式,但这并不能取代Qt中的信号和槽,更不能使其过时。
自Qt 5起,槽方面变得不那么相关了,因为可以将信号连接到任何函数。但是,槽仍然与Qt元系统集成,许多Qt API使用该元系统来使事情正常运行。
是的,你可以使用回调来实现信号所能实现的几乎所有内容。但这并不简单,而且需要更多的代码量,它不会自动处理排队的连接,也无法像信号那样与Qt集成,虽然可以解决此问题,但代码量会更多。
在QML中,信号和槽基本上是Qt的主要焦点,所以我认为信号会一直存在。
信号和槽更好是因为Qt的概念就是围绕它们构建的,它们是API的一部分,并被许多API使用。这些概念在Qt中已经存在了很长时间,早在C++没有提供除继承自C的普通函数指针之外的回调支持时就已经有了。这也是为什么Qt不能简单地切换到std回调的原因 - 它会破坏很多东西并且是不必要的努力。同样的原因,Qt仍然使用那些邪恶的不安全的普通指针而不是智能指针。从概念上讲,信号和槽并不过时,在使用Qt时技术上更加如此。C++只是太迟了。现在期望每个人都立即从他们的庞大代码库中移动开来并采用标准库中的替代方法是不现实的。

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

话虽如此,您可以通过简单的回调实现所有这些功能,但这就像重新发明轮子。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接