Why are signals and slots better than plain old callbacks?
Asked Answered
S

3

13

Newbie to C++ here. I was reading 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

Here is that code rewritten using callbacks:

#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);

As you can see, the callback version is type-safe and shorter than the Qt version, 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) to work. Why, then, are signals and slots preferred over callbacks? Has C++11 simply obsoleted these concepts?

Thanks.

Summitry answered 12/12, 2015 at 6:52 Comment(11)
Qt is much older than C++11. The claims in the Qt documentation, whitepapers etc. must be read as claims for "C++ as a better C" previous to the C++11 standard. In particular silly claims like "callbacks have two fundamental flaws: Firstly, they are not type-safe." just make no sense for C++ as C++: to understand it one must think in terms of pure C programming.Exhalation
Don't conflate "Qt's implementation of signals and slots" with "signals and slots" the concept. When something maintain as list of callbacks and calls them all when an event occurs. you have the essence of signals and slots. Everything else is sugar, syntax, and marketing. What you have is a fine, if simplistic, implementation of a signal and slot system.Absence
@Absence Then what exactly is the difference between my implementation and Qt's?Summitry
Signal slot or event delegate pattern, which give you a better abstraction. I think It has some advantages: 1. type safe; 2. automatically manage object, so if the observable or the observer is deleted, the connection will be destroyed automatically ; 3: a high level abstraction and better concept for inter-object communication.Pileus
I think there is a fundamental difference between callbacks and signal/slot concept. They're made for different use cases. Callbacks are simpler, but they're single ended: there's no equivalent of a slot. The slot is necessary for correct handling of the situation when the signal receiver ceases to be. It's easy with slots, but not as easy with callbacks when it comes to handling the deletion of the callee. I would know, I've written both my own safe signal / slot system and a safe callback system.Dunbarton
It's worth noting that usually what is meant by "callbacks" here is not function objects, but C-style function pointers. That's what leads to the lack of type safety -- not lambdas captured to function objects or functionoids with state, but because function pointers, to accept a generalized set of parameters, typically have to squeeze data through something akin to a void pointer.Tinned
In today's day and age with C++11 and on with shared_ptr and lambdas, closures, and std::function, it would probably be better and even easier (less boilerplate, more flexibility, and reduced MOC reliance) to use a signals and slots implementation based on standard C++ concepts. But that wasn't true when Qt was originally conceived.Tinned
@Ike True, but how would even a C-style function pointer be type unsafe? In the second example, couldn't std::function<void(int)> be replaced by void(*)(int), and the lambda replaced by a method declaration, and it'd still be type-safe?Summitry
@JamesKo The thing about C-style callbacks is that they can't capture state like a C++ function object/functionoid. So when you want to pass something through from where an event occurs to where the event is handled in a kind of generic way, you have to pass it through something like void* or a variant or something of that sort as an argument -- and then cast the data back to its original form at the callback site in which the event is handled. That's where the type safety is lost, at least at compile-time, and both compile-time and runtime if void* is used.Tinned
@JamesKo Let's say you have a C-style GUI API and you want to do something when a button is pushed. In that case, you pass a function pointer. However, when the button is pushed and your function pointer is called, you want access to some data in your application. To do that generically in C, you typically have to pass it through a void pointer, casting it to void*, then when your function pointer is called, it might have a signature like void (*event)(void* user_data). You then have to cast the user_data to what you passed in earlier (the app data you wanted to access).Tinned
@JamesKo Now in your particular example, you do have a signature that accepts int. But the problem is that your lambdas are closures accessing these Counters. To design a general API where you can access those counters or anything else from within a C-style function pointer callback, they'd typically have to be squeezed through a void pointer, or you'd have to lose the generality completely and design an API which only works with counters when an event is triggered. Either that or you have to make your counters into global variables. Hope that makes sense!Tinned
T
5

Why are signals and slots better than plain old callbacks?

Because signals are a lot like plain old callbacks, on top of having extra features and being deeply integrated with Qt APIs. It ain't rocket science - callbacks + extra features + deep integration is greater than callbacks alone. C++ might be finally offering a cleaner way to do callbacks, but that doesn't replace Qt signals and slots, much less render them obsolete.

The slot aspect got a little less relevant since Qt 5, which allowed signals to be connected to any functions. But still, slots integrate with the Qt meta system, which is used by a lot of Qt APIs to get things working.

Yes, you could use callbacks for pretty much everything which signals are supposed to achieve. But it is not easier, it is a little more verbose, it doesn't automatically handle queued connections, it won't integrate with Qt the way signals do, you could probably work around that as well, but it will get even more verbose.

And in the case of QML, which nowadays is the primary focus of Qt, you are essentially stuck with Qt's signals. So I presume signals are here to stay.

Signals and slots are "better" because Qt is conceptually built around them, they are part of the API and are used by a lot of the APIs. Those concepts have been in Qt for a long time, from back in the days C++ didn't offer much callback support aside from plain old function pointers it inherited from C. This is also the reason Qt cannot simply switch to std callbacks - it will break a lot of stuff and is a needless effort. The same reason Qt continues to use those evil unsafe plain old pointers instead of smart pointers. Signals and slots are not obsolete as a concept, even less so technically when using Qt. C++ simply got too late in the game. It is unrealistic to expect that everyone will now rush into moving away from their own implementations in their giant code bases now that C++ finally provides alternatives as part of the language standard library.

Tabbitha answered 12/12, 2015 at 11:52 Comment(0)
B
18

There's one enormous difference between the two: threads.

Traditional callbacks are always called in the context of the calling thread. Not so with signals and slots -- as long as the thread is running an event loop (as it will be if it's a QThread) the slot can be in any thread.

Sure, you can do all of this manually with a callback -- I've written many Win32 apps over the years that use Windows-style message pumps that juggle callbacks across threads -- but it's a lot of boilerplate code and not much fun to write, maintain, or debug.

Butterfat answered 12/12, 2015 at 17:35 Comment(0)
T
5

Why are signals and slots better than plain old callbacks?

Because signals are a lot like plain old callbacks, on top of having extra features and being deeply integrated with Qt APIs. It ain't rocket science - callbacks + extra features + deep integration is greater than callbacks alone. C++ might be finally offering a cleaner way to do callbacks, but that doesn't replace Qt signals and slots, much less render them obsolete.

The slot aspect got a little less relevant since Qt 5, which allowed signals to be connected to any functions. But still, slots integrate with the Qt meta system, which is used by a lot of Qt APIs to get things working.

Yes, you could use callbacks for pretty much everything which signals are supposed to achieve. But it is not easier, it is a little more verbose, it doesn't automatically handle queued connections, it won't integrate with Qt the way signals do, you could probably work around that as well, but it will get even more verbose.

And in the case of QML, which nowadays is the primary focus of Qt, you are essentially stuck with Qt's signals. So I presume signals are here to stay.

Signals and slots are "better" because Qt is conceptually built around them, they are part of the API and are used by a lot of the APIs. Those concepts have been in Qt for a long time, from back in the days C++ didn't offer much callback support aside from plain old function pointers it inherited from C. This is also the reason Qt cannot simply switch to std callbacks - it will break a lot of stuff and is a needless effort. The same reason Qt continues to use those evil unsafe plain old pointers instead of smart pointers. Signals and slots are not obsolete as a concept, even less so technically when using Qt. C++ simply got too late in the game. It is unrealistic to expect that everyone will now rush into moving away from their own implementations in their giant code bases now that C++ finally provides alternatives as part of the language standard library.

Tabbitha answered 12/12, 2015 at 11:52 Comment(0)
W
1

In general: Signals and Slots differs from callbacks by the fact that it decouples the call (Signal) from the Handler (Slot). Which means: you can register your slot to be on a different thread, you can listen to one signal from many slots and change queuing strategy easily. but it has its costs(In QT world at least...): string evaluation and generally more internal work / code branches.. in short, it's an higher level concept.

that being said, you can do all of those with simple callbacks, but it will be like reinventing the wheel.

Woodworking answered 30/11, 2020 at 10:29 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.