It shows that C++ was considered too complex already in the 90s.
That may be the case, but there are plenty of examples of elegant implementations.
JUCE, for instance:
#include <juce_core/juce_core.h>
class MyComponent {
public:
void doAsyncOperation(std::function<void(int)> callback) {
// Simulate async work
juce::MessageManager::callAsync([callback]() {
callback(42); // Call the functor with result
});
}
};
// Usage
MyComponent comp;
comp.doAsyncOperation([](int result) {
juce::Logger::writeToLog("Callback received with: " + juce::String(result));
});
.. I think that's kind of clean and readable, but ymmv, I guess?But it's important to see the 1994 (and 1998) view of the world to understand how modern c++ features work. Because they start from that worldview and start adding convenient stuff. If you don't understand how c++ used to work, you may be confused with why c++ lambdas look so weird.
(i) Have they thought about the relative lifetimes of the sender and receiver?
(ii) Is the callback a "critical section" where certain side-effects have undefined behavior?
(iii) Does the functors store debugging info that .natvis can use?
(iv) Is it reeeeeeeally that bad to just implement an interface?
Regarding your fourth point, sometimes an architecture can be vastly simplified if the source of information can abstracted away. For example, invoking a callback from a TCP client, batch replay service, unit test, etc. Sometimes object oriented design gets in the way.
To your first point, I think RAII and architecture primarily address this. I'm not sure that I see callback implementation driving this. Although I have seen cancellable callbacks, allowing the receiver to safely cancel a callback when it goes away.
Common implementations are a function pointer + void* pair, which in most debuggers just show you two opaque addresses. Better to include a info block -- at least in debug builds -- with polymorphic type pointers that can actually deduce the type and show you all the fields of the receiver.
>> sometimes an architecture can be vastly simplified if the source of information can abstracted away.
"sometimes" is doing a lot of heavy lifting here. That's my whole point -- more often than not I see some type of homespun functor used in cases that are _not_ simplified, but actually complicated by the unnecessary "plumbing."
>> RAII and architecture primarily address this
If the receiver uses RAII to clean up the callback, then you've reintroduced the "type-intrusiveness" that functors are meant to avoid...?
This has not been my experience. But I haven't needed to deal with RTTI disabled.
By RAII, I mean using destructors to unregister a callback. This covers 99.9% of use cases. Generally callback registration is not where you really want type erasure anyways.
Callbacks in C++ using template functors (1994) - https://news.ycombinator.com/item?id=18650902 - Dec 2018 (50 comments)
Callbacks in C++ using template functors – Rich Hickey (1994) - https://news.ycombinator.com/item?id=12401400 - Aug 2016 (1 comment)
Callbacks in C++ using template functors (1994) - https://news.ycombinator.com/item?id=10410864 - Oct 2015 (2 comments)
zengid•2h ago
[0] https://youtu.be/wo84LFzx5nI?si=SBv1UqgtKJ1BH3Cw&t=5159