I used many of the facilities of Juce to write my program, even when I had to write adaptors to other libraries to do so and while I have my quibbles it worked out well for me every time.
There is, however, one facility that I instantly decided to roll my own, and that was the whole callback/update/listener mechanism. The simple reason is type safety - I like to be very type-safe in my code, I save my sloppy ass all the time that way without paying attention, and I had repeated troubles not making mistakes until I went to a fully typed callback.
Here’s an example, distilled from real code. Consider that juce::AudioTransportSource inherits from both juce::ChangeBroadcaster and juce::PositionableAudioSource, you might be tempted to attach the following listener to it:virtual void changeCallback(ChangeBroadcaster* objectThatChanged) {
PositionableAudioSource* source = (PositionableAudioSource*) objectThatChanged;
}
But you’d be wrong - like I was (in my defense, the cast came at another place) and you’d likely experience a SEGV or the like. The reason (more or less - details will vary on individual compilers) is that objectThatChanged points to the base of the vtable of ChangeBroadcaster which is almost certainly not the base of the vtable of PositionableAudioSource.
A temporary hack that works in that case would be casting lower, to the actual class AudioTransportSource. I’m pretty sure that compiler is then able to work its magic correctly…
…but that’s a bad solution. I’m not convinced that the compiler has to do a good job at this - it didn’t on the original code, that looks so similar - and I can still later attach a listener to some other class that’s both a ChangeBroadcaster and a PositionableAudioSource (and there are several!) and again get a run-time signal.
Even before I found that bug, I was already using my own generic listeners almost everywhere between my compilation units - simply because it was far more convenient:template <typename Type>
class Listener {
public:
virtual void operator()(Type x) = 0;
virtual ~Listener() {}
};
(And of course there’s a corresponding broadcaster…)
This is lighter and more flexible - because it uses call by value. Of course, that “value” can be a pointer or reference, I have tons of interfaces like Listener<PositionableAudioSource*> and Listener<const Preferences&> and things like that - but you can have things like Listener or Listener<const char*>.
As an example, everything in my code that listeners to the current time, like cursors or clocks, is a Listener. There’s no object that needs to be passed around, no locking… I get just the time delivered to my method.
And I love typesafety so it’s likely that “after the rush” I’m going to create this tiny struct:struct Time {
float time_;
Time(float time) : time_(time) {}
operator float() const { return time_; }
}; and then have those time-listeners become Listener - at no additional CPU or memory cost (in fact, if I make the change correctly it might be that the code is byte-for-byte the same…).
Note that I use operator() here! This might be controversial… and in fact, you can use the generic listeners without having to use operator() as the name of the single virtual function.
So if you don’t like operator(), consider strongly using generic listeners, but with a named virtual function.
But operator() is so extremely convenient in generic code because you can intermix pure functions and objects, and once you’re used to it, it makes the code shorter and clearer.
It means, for example, that a generic callback in your code can either be a function, or a class with an operator() function - and that’s so darned convenient! And it interoperates well as a result with a lot of other generic packages, like STL.
