Goodbye Listeners and Callbacks, Hello Lambdas?

Are there any plans to modernize the callback system used for GUI elements to take lambdas, now that you’re using c++11 for Juce5? It was mentioned here by @otristan :

There’s at least one downside to using lambdas/std::functions for the callbacks, namely that is tricky to remove them later after they’ve been set. Still, I think it would be quite valuable to have support for lambdas for the basic callbacks from buttons, sliders and similar components.

This was a large part of my talk last year at ADC but the video hasn’t made it online yet…

You can see the notes here though: https://github.com/drowaudio/presentations/blob/master/ADC%202016%20-%20Using%20Modern%20C%2B%2B%20to%20Improve%20Code%20Clarity/Using%20Modern%20C%2B%2B%20to%20Improve%20Code%20Clarity.pdf (Look for ButtonClickCallback on slide 28)

Why is it hard to remove the callbacks? Just set them to nullptr?

5 Likes

If the system needs support for multiple “listeners”…

This pdf is amazing!!! Why isn’t any of this already in JUCE? that PopupMenu Callbacks!!! come on now! that is amazing!

1 Like

Yes, I agree with that but to be honest since I’ve started using this style I’ve never actually needed to attach more than one listener to something.

The more common and dare I say “better” approach would be to have a single button callback update your model and then other listeners respond to this.

I’m not saying I’d replace the ListenerList system, on the contrary there are many examples of its use in that talk but for simple UI elements (pop up menus, sliders, buttons etc.) lambda based callbacks can be much more terse and quicker to use.

Just for the discussion, what would be some instances where you would need to attach multiple listeners to a component/object?

Whatever use cases there are for the current Juce Components to support multiple listeners. The method is named addListener, after all, not just setListener…

oh, like if you have a component with several buttons as children? and the component is who listens for button interaction, not the buttons themselves?

But wouldn’t being able to attach lambdas directly to the buttons eliminate that problem?

In my opinion, the best way (that I know of) to deregister lambda listeners is to let the AddListener function return a “deregister function” that can be called any time later (with no arguments). I replaced my Listener classes with lambdas and this deregister method some time ago and never looked back :slight_smile:

5 Likes

please share the code!!! @idearcos

@matkatmusic Just in case my post was misleading, I meant that I replaced listener with lambdas in my own code, not in the interaction with Juce. Sorry if anyone thought I applied this method directly to Juce.

However, just in case anyone is interested, let me add a quick example of it here. You can easily implement it in your own code, and I think it could be a good alternative way to use listeners in Juce too, if the users and developers like it.

class Foo
{
public:
    using Listener = std::function<void(int value)>;

    std::function<void()> AddListener(Listener &&listener_function)
    {
        auto it = listeners_.emplace(listeners_.begin(), listener_function);
        return [this, it]() { listeners_.erase(it); };
    }

private:
    std::list<Listener> listeners_;
}

Edit: fixed a typo

5 Likes

Yes, and it may be convenient at times, but in other circumstances doing so would spread around code that would be more maintainable and readable if it were centralized in a parent/handler object.

I think that adding lambda support alongside keeping the possibility to register listeners is the best way to go.
It doesn’t break existing code, and gives the possibility to choose the approach that better suits the problem at hand without forcing a “policy”.

2 Likes

http://timj.testbit.eu/2013/01/25/cpp11-signal-system-performance/

1 Like

Hi @dave96,

Wondering if you still agree with the patterns you showcased in this presentation or if you’ve moved to a different listener/observer pattern?

We started using a similar pattern of having public std::functions in classes that need to be listened to and then assign lambdas to them like so:

m_thing.onStuffHappened = [this]() { doSomething() };

We’ve come to dislike this pattern largely because it requires putting code in the constructor that’s not actually called in the constructor. For example:

MyWidget()
{
    addAndMakeVisible (m_button1);
    m_button1.onClick = [this]() {
        auto isButtonToggled = m_button1.getToggleState();
        handleButton1 (isButtonToggled);
    };

    addAndMakeVisible (m_button2);
    m_button2.onClick = [this]() {
        auto isButtonToggled = m_button2.getToggleState();
        handleButton2 (isButtonToggled);
    };
}

When reading through the code it’s not immediately obvious that those lines of code aren’t actually run during construction.

Another method we’ve tried is binding member functions to the std::functions, something like:

MyWidget::MyWidget()
{
    m_button1.onClick = std::bind(&MyWidget::onButtonClicked, this);
}

void MyWidget::onButtonClicked()
{
}

But this is not so good when you have multiple things to listen to, such as buttons, as there’s no way to tell which object the callback is coming from - without having multiple member functions for each object which is just messy.

So long question short, what pattern are you currently using for this?

I use the constructor pattern. Tbh I have no problem setting up the behaviour there. Usually the body of the lambda is only a single line, a call to another function, if it is bigger then you probably want to break that behaviour out in to another function.

Rather than name that function onButtonClicked , I’d probably name it as to what it does e.g. refreshList etc. so it doesn’t get confusing and you can call that function from other places without it being specific to the button.

If you really do want a single callback for all your buttons though, I probably wouldn’t use std::bind, just pass the button to the function like a juce::Button::Listener: m_button1.onClick = [this] { onButtonClicked (m_button1); };. But as I said it might be better to use separate functions for each button or even pass an enum rather than the buttons, they are all clearer ways to show what the code is doing.

3 Likes

I wanted to concur with Dave. It’s very obvious that the lambda is what will happen whenever the button is clicked, via the ‘onClick’ at the start of the line.

The constructor approach is akin to saying “I’m setting up what will happen when you click this button”

Perhaps slightly off-topic, but one approach I’ve used in the past is to store a ScopedValueSetter to automatically assign and later zero-out the callback. This way, you don’t need to worry about dangling references if your controller/listener class is destroyed before your view class.

struct MyModel
{
    void switchMode() {}
};

struct MyView : public juce::Component
{
    juce::TextButton button;
};

class MyController
{
public:
    MyController (MyView& view, MyModel& model)
        : scopedOnClick (view.button.onClick, [&model] { model.switchMode(); })
    {}

private:
    juce::ScopedValueSetter<std::function<void()>> scopedOnClick;
};
8 Likes

We haven’t completely ruled out using lambdas yet so it’s useful to hear you’re still in favour of them. Naming the functions something more meaningful is certainly important.

With a real-world example, we just update our model directly from the assigned lambda:

m_sampleRateSettings.onSampleRateChanged = [this]() {
    m_model.setSampleRate (m_sampleRateSettings.getSampleRate());
};

(Where m_model is essentially a wrapper around a juce::ValueTree and a bunch of juce::CachedValues, thanks to another of your talks!)

@matkatmusic Once you take the time to stop and read it properly then yes, it’s easy to see that the code is simply being assigned to a callback, but we feel it still adds unnecessary visual clutter in the constructor. I suppose one alternative might be to use a free function to set up the widgets…

@reuk Interesting method, I’ve not used ScopedValueSetter before. Usually the thing we’re listening to is a member of the thing that’s doing the listening so there’s no need to worry about the callback going out of scope. Also interesting that you’ve called a custom component a ‘view’. We tend to think of components as controllers and the LookAndFeel as the view… but that’s getting even more off topic!

Yeah, if there is a lot of setup to do, you can stick it in a named function and call it from your constructor.
The only draw back of that is that it could get called again from somewhere else. Keeping it in the constructor is the “correct” place for it.

I guess it really depends on the size of your constructor and what else your’re doing in there.
These days, C++ can be so terse and with RAII, we try to keep our constructors pretty small, usually just binding callbacks etc. for a UI component. If you have a huge constructor, you might want to think about splitting it up in to multiple components?

It’s all a tradeoff really!


@reuk nice trick with the ScopedValueSetter, I know a lot of people ask for a clean way to do that so it’s nice to know one is available out the box. Although I’m usually with @ImJimmi in that I’m attaching callbacks to local components so it’s not usually a problem we have.

4 Likes