#Passing lambdas to a class method for storage

153 messages · Page 1 of 1 (latest)

hollow osprey
#

I want the addAction method to support 0 or more arguments for the lambda functions being passed in. I get the error shown below even if I remove <TrackSelect>from the method call. Caveat, I don't want to use std::function because this is meant for an embedded platform.

In file included from include/melseq.h:12,
                 from src/melseq.cpp:1:
include/menu.h:85:10: note: candidate: 'template<class ... Payload> void Menu::addAction(MenuActionId, void (&&)(GUI*, Payload ...), void (&&)(GUI*, Payload ...))'
   85 |     void addAction(MenuActionId id, Callback<Payload...> &&onActive, Callback<Payload...> &&onDefault)
      |          ^~~~~~~~~
include/menu.h:85:10: note:   template argument deduction/substitution failed:
src/melseq.cpp:38:37: note:   mismatched types 'Menu::Callback<Payload ...>' and 'Melseq::setup(Adafruit_SH1107*)::<lambda(GUI*, TrackSelect)>'

The calling code:

mainMenu_.addAction<TrackSelect>(
    MenuActionId::TrackSelect,
    [](GUI *gui, TrackSelect payload)
    {
        gui->drawCell(payload.cell, {1, 1}, 1);
        gui->drawStringAtCell(payload.cell, {1, 1}, payload.name.data(), 1);
    },
    [](GUI *gui, TrackSelect payload)
    {
        gui->drawCell(payload.cell, {1, 1}, 0);
        gui->drawStringAtCell(payload.cell, {1, 1}, payload.name.data(), 0);
    }
);

The implementation code:

class Menu {
// ... stuff

template <typename... Payload>
using Callback = void(GUI *, Payload...);

template <typename... Payload>
void addAction(MenuActionId id, Callback<Payload...> &&onActive, Callback<Payload...> &&onDefault)
{
    actions_.emplace_back(id, etl::move(onActive), etl::move(onDefault));
}

// stuff
}
burnt charmBOT
#

When your question is answered use !solved to mark the question as resolved.

Remember to ask specific questions, provide necessary details, and reduce your question to its simplest form. For tips on how to ask a good question use !howto ask.

heavy nacelle
#

Storing arbitrarily sized data without some sort of indirection will not work. Can you limit the callbacks to non-capturing lambdas?

#

Making a compilable example that shows the error on godbolt.org would probably make helping easier.

hollow osprey
#

The lambdas don't capture any context if that's what you mean?

heavy nacelle
#

It is. If you're effectively only storing function pointers you can avoid std::function.

#

As a little trick, add a + in front of the lamdas. It'll convert them to a function pointer and might make the error message more useful.

hollow osprey
#

I should have disclosed that actions_ is an etl::vector which stores MenuAction, defined as:

template <typename... Payload>
class MenuAction
{
private:
    MenuActionId id_;
    MenuActionState state_ = MenuActionState::None;

    typedef void (*Callback)(GUI *, Payload...);

    Callback onActive;
    Callback onDefault;

public:
    MenuAction() = default;
    MenuAction(MenuActionId id) : id_(id) { state_ = MenuActionState::Default; };
    MenuAction(MenuActionId id, Callback &&onActive, Callback &&onDefault) : id_(id)
    {
        onActive = etl::move(onActive);
        onDefault = etl::move(onDefault);
    }

    ~MenuAction() = default;

    void trigger(MenuActionState state) { state_ = state; }

    void update(GUI *gui, Payload... payload)
    {
        switch(state_)
        {
            case MenuActionState::Active: onActive(gui, payload...); break;
            case MenuActionState::Default: onDefault(gui, payload...); break;
            default: break;
        }
    }

    const MenuActionId &id() const { return id_; }
};
#

In a nutshell I want to define the lambda where addAction is called, but then the ownership is transferred to a MenuAction which lives inside a Menu

#

A part of the error changed to:
mismatched types 'Menu::Callback<Payload ...>' and 'void (*)(GUI*, TrackSelect)'

heavy nacelle
#

Does it tell you what type Menu::Callback<Payload ...> really is? It should be similar.

neat light
#

would be a tad simpler with the complete error

hollow osprey
#

I changed these:

etl::vector<MenuAction<>, 2> actions_;

template <typename... Payload>
    using Callback = void (*)(GUI *, Payload...);

    template <typename... Payload>
    void addAction(MenuActionId id, Callback<Payload...> &&onActive, Callback<Payload...> &&onDefault)
    {
        MenuAction<Payload...> action{id, etl::move(onActive), etl::move(onDefault)};

        actions_.push_back(action);
    }
neat light
hollow osprey
#

Now the error shifted to push_back so the function signature works

#
In file included from include/melseq.h:12,
                 from src/melseq.cpp:1:
include/menu.h: In instantiation of 'void Menu::addAction(MenuActionId, void (*&&)(GUI*, Payload ...), void (*&&)(GUI*, Payload ...)) [with Payload = {TrackSelect}; Callback<Payload ...> = void (*)(GUI*, TrackSelect)]':
src/melseq.cpp:38:24:   required from here
include/menu.h:92:27: error: no matching function for call to 'etl::vector<MenuAction<>, 2>::push_back(MenuAction<TrackSelect>&)'
   92 |         actions_.push_back(action);
      |         ~~~~~~~~~~~~~~~~~~^~~~~~~~
In file included from /Users/phoenix/Projects/cpp/audio/playa-cpp/include/playa/constants/notevalues.h:7,
                 from include/melseq.h:5:
.pio/libdeps/adafruit_feather/Embedded Template Library/include/etl/vector.h:434:10: note: candidate: 'void etl::ivector<T>::push_back(const_reference) [with T = MenuAction<>; const_reference = const MenuAction<>&]'
  434 |     void push_back(const_reference value)
neat light
#

if callback is a pointer to function, it's awkward to take an rvalue reference to it

hollow osprey
#

callback is a function pointer, but is there an alternative? Is there a function value?

neat light
neat light
hollow osprey
#

The vector needs to know the size and it doesn't know..

neat light
#

there's no "function value", but a pointer to function isn't a function value, it's a pointer to a function, and if you need a pointer you typically take the pointer by value

neat light
hollow osprey
neat light
hollow osprey
neat light
#

sure and you're exactly not doing what they're doing

#

they take an lvalue rvalue reference to their custom type, which is move aware, because they explicitly want to tap into move semantics

#

you're taking a pointer, and there's no point to take it by rvalue reference

#

in fact it's generally not useful to take a pointer by any reference type, unless you wish to modify the original input pointer

#

if you're not planning on doing that, then it's potentially harmful, and just not useful

hollow osprey
neat light
#

or to put it differently, it's similar to you requesting a T** while you would have been fine with just taking a T*

hollow osprey
#

Cheers, that makes sense

#

The unfortunate thing about the etl::vector problem at the moment is that the MenuAction class doesn't store anything, it's just plumbing :/

neat light
#

getting back to the vector thing I'm not completely sure what the status of the discussion is

neat light
hollow osprey
#

After applying your remarks to the function signature of addAction, this is the error:

In file included from include/melseq.h:12,
                 from src/melseq.cpp:1:
include/menu.h: In instantiation of 'void Menu::addAction(MenuActionId, Callback<Payload ...>, Callback<Payload ...>) [with Payload = {TrackSelect}; Callback<Payload ...> = void (*)(GUI*, TrackSelect)]':
src/melseq.cpp:38:24:   required from here
include/menu.h:89:27: error: no matching function for call to 'etl::vector<MenuAction<>, 2>::push_back(MenuAction<TrackSelect>&)'
   89 |         actions_.push_back(action);
      |         ~~~~~~~~~~~~~~~~~~^~~~~~~~
In file included from /Users/phoenix/Projects/cpp/audio/playa-cpp/include/playa/constants/notevalues.h:7,
                 from include/melseq.h:5:
.pio/libdeps/adafruit_feather/Embedded Template Library/include/etl/vector.h:434:10: note: candidate: 'void etl::ivector<T>::push_back(const_reference) [with T = MenuAction<>; const_reference = const MenuAction<>&]'
  434 |     void push_back(const_reference value)
      |          ^~~~~~~~~
.pio/libdeps/adafruit_feather/Embedded Template Library/include/etl/vector.h:434:36: note:   no known conversion for argument 1 from 'MenuAction<TrackSelect>' to 'etl::ivector<MenuAction<> >::const_reference' {aka 'const MenuAction<>&'}
  434 |     void push_back(const_reference value)
      |                    ~~~~~~~~~~~~~~~~^~~~~
.pio/libdeps/adafruit_feather/Embedded Template Library/include/etl/vector.h:448:10: note: candidate: 'void etl::ivector<T>::push_back(rvalue_reference) [with T = MenuAction<>; rvalue_reference = MenuAction<>&&]'
  448 |     void push_back(rvalue_reference value)
      |          ^~~~~~~~~
.pio/libdeps/adafruit_feather/Embedded Template Library/include/etl/vector.h:448:37: note:   no known conversion for argument 1 from 'MenuAction<TrackSelect>' to 'etl::ivector<MenuAction<> >::rvalue_reference' {aka 'MenuAction<>&&'}
  448 |     void push_back(rvalue_reference value)
neat light
#

also it sounds like your vector type is just what is commonly called a (static) array

neat light
hollow osprey
neat light
neat light
hollow osprey
hollow osprey
#
#

ETL is like std but for embedded development, there's no heap usage.

heavy nacelle
#

You don't need dynamic memory if you don't capture anything. Having an array of function pointers is fine.

#

You don't even need a fancy vector with constructing and destructing elements.

hollow osprey
#

I'll try to put together a compile explorer example

neat light
hollow osprey
heavy nacelle
#

Sounds like you want an array.

neat light
#

unless you for some reason want to type erase the stuff, which also usually mean you want to manually track the type in some way or via some side channel, the simplest "fix" is to just use the correct type

hollow osprey
#

I want to do this:

mainMenu_.addAction(
    MenuActionId::TapTempo,
    [](GUI *gui)
    {
        gui->drawCell({5, 0}, {1, 1}, true);
        gui->drawTapIcon({5, 0}, true);
    },
    [](GUI *gui)
    {
        gui->drawCell({5, 0}, {1, 1});
        gui->drawTapIcon({5, 0});
    }
);

mainMenu_.addAction(
    MenuActionId::TrackSelect,
    +[](GUI *gui, TrackSelect payload)
    {
        gui->drawCell(payload.cell, {1, 1}, 1);
        gui->drawStringAtCell(payload.cell, {1, 1}, payload.name.data(), 1);
    },
    +[](GUI *gui, TrackSelect payload)
    {
        gui->drawCell(payload.cell, {1, 1}, 0);
        gui->drawStringAtCell(payload.cell, {1, 1}, payload.name.data(), 0);
    }
);
neat light
#

if for some reason you need that vector/etl to store elements of varying types, then you have to revise your design or make a design that is able to work without knowing the concrete type

#

why

heavy nacelle
#

Currently you can't do that because the lambdas don't take the same arguments. You wouldn't be able to call them.

neat light
#

how do you keep track of which "callbacks" take argument or not

#

it's just not possible to use the callbacks properly without knowing what argument(s) they take, so your setup, in some way, need to be able to track that

#

and from what you've shown so far, there's no information about how you're doing that

#

so it just sounds like you're in fact not doing that

heavy nacelle
#

One way is to add a dummy TrackSelect to the first lambdas just to make them have the same signature.

hollow osprey
#

First half:

#pragma once

#include "gui.h"
#include <utility>
#include <etl/observer.h>
#include <etl/delegate.h>
#include <etl/delegate_service.h>
#include <etl/vector.h>

enum class MenuActionId : uint8_t
{
    TapTempo,
    TrackSelect
};

enum class MenuActionState : uint8_t
{
    Active,
    Default,
    None,
};

struct MenuActionEvent
{
    MenuActionId id;
    MenuActionState type = MenuActionState::None;
};

template <typename... Payload>
class MenuAction
{
private:
    MenuActionId id_;
    MenuActionState state_ = MenuActionState::None;

    typedef void (*Callback)(GUI *, Payload...);

    Callback onActive;
    Callback onDefault;

public:
    MenuAction() = default;
    MenuAction(MenuActionId id) : id_(id) { state_ = MenuActionState::Default; };
    MenuAction(MenuActionId id, Callback onActive, Callback onDefault) : id_(id)
    {
        onActive = etl::move(onActive);
        onDefault = etl::move(onDefault);
    }

    ~MenuAction() = default;

    void trigger(MenuActionState state) { state_ = state; }

    void update(GUI *gui, Payload... payload)
    {
        switch(state_)
        {
            case MenuActionState::Active: onActive(gui, payload...); break;
            case MenuActionState::Default: onDefault(gui, payload...); break;
            default: break;
        }
    }

    const MenuActionId &id() const { return id_; }
};
neat light
#

another way is to just have separate storage

hollow osprey
#

Second half

class Menu
{
private:
    /* data */
    GUI *gui_;
    etl::vector<MenuAction<>, 2> actions_;

public:
    Menu() = default;
    Menu(GUI *gui) : gui_(gui) {}

    ~Menu() = default;

    template <typename... Payload>
    using Callback = void (*)(GUI *, Payload...);

    template <typename... Payload>
    void addAction(MenuActionId id, Callback<Payload...> onActive, Callback<Payload...> onDefault)
    {
        MenuAction<Payload...> action{id, onActive, onDefault};

        actions_.push_back(action);
    }

    // void addAction(
    //     MenuActionId id, etl::delegate<void(GUI *)> &&onActive, etl::delegate<void(GUI *)> &&onDefault
    // )
    // {
    //     MenuAction<> action{id};

    //     action.onActive = etl::move(onActive);
    //     action.onDefault = etl::move(onDefault);

    //     actions_.push_back(etl::move(action));
    // }

    void triggerAction(MenuActionId id, MenuActionState type)
    {
        for(auto &action : actions_)
        {
            if(action.id() == id)
            {
                action.trigger(type);
                break;
            }
        }
    }

    template <typename... Args>
    void update(Args... args)
    {
        for(auto &action : actions_)
        {
            action.update(gui_, args...); // Pass the arguments to MenuAction::update
        }
    };
};
heavy nacelle
#

Another way is to give up on the dynamic part and just do

struct MainMenu {
  MenuAction<GUI*> menu1;
  MenuAction<GUI*, TrackSelect> menu2;
};
neat light
#

this fundamentally makes no sense

    template <typename... Args>
    void update(Args... args)
    {
        for(auto &action : actions_)
        {
            action.update(gui_, args...); // Pass the arguments to MenuAction::update
        }
    };
#

it just doesn't

#

what do you expect this piece of code to do, if you call it with no argument, and run into an "action" that takes a TrackSelect

#

or vice versa you pass in a TrackSelect, and run into an "action" that takes no argument

neat light
#

no extra argument (just GUI*) and one extra argument of type TrackSelect

hollow osprey
#

Let me start by saying you're right, that was from some LLM when I was trying to debug the issue. The correct code should be:

  void update()
    {
        for(auto &action : actions_)
        {
            action.update(gui_); // Pass the arguments to MenuAction::update
        }
    };
neat light
neat light
#

so yeah in that case you can use one single container to store all the actions, if you do it right

hollow osprey
#

I got lost in the method calling I didn't really address the plumbing

#

First I wanted to know if I could call addAction like I'm doing with different length arguments in the lambda. That was the initial question, should have stopped there. Thanks for the help guys.

burnt charmBOT
#

@hollow osprey Has your question been resolved? If so, type !solved :)

hollow osprey
#

Simply enough, triggerAction is already doing something similar, therefore update just needs to be:

    template <typename... Payload>
    void update(MenuActionId id, Payload... payload)
    {
        for(auto &action : actions_)
        {
            if(id == action.id())
            {
                action.update(gui_, payload...); // Pass the arguments to MenuAction::update
                break;
            }
        }
    };
heavy nacelle
#

Why is update a template?

hollow osprey
#

This is a really normal composition pattern, I'm just not an expert in C++.

heavy nacelle
hollow osprey
heavy nacelle
#

What stops you from doing mainMenu_.update(MenuActionId::TrackSelect);?

neat light
#

if you already have knowledge of what types/ids/payloads you'll get it's just simpler to split those in different containers

#

then you don't check the id at all, you just iterate on the one container that has the right stuff

#

if for whatever reason you need them to all be in the same container, you pretty much need to "erase" the type for the container to store them all properly

hollow osprey
neat light
#

you cannot not know anything about this

#

and are you really doing a GUI thing in embedded and not wanting to use std::function?

#

like what's the actual restriction here

#

is it about dynamic memory or just plain availability

hollow osprey
#

And this mentions, do not use std::function

#

The restriction here is heap allocation. There's no heap. When the binary is compiled it has all the memory it needs ahead of time allocated to the stack.

#

There are also no exceptions if that matters.

neat light
#

no, it's just that the concept of gui in embedded just sounds strange to me

hollow osprey
#

The GUI is shown in an OLED screen

heavy nacelle
#

It happens. I had to make one too 🤷

neat light
#

anyway, your update thing still isn't going to work the way you've written it

hollow osprey
#

I think in the end I have to resort to https://www.etlcpp.com/observer_tutorial.html pattern and define the Menu with all the possible payloads.

#

Or something of that sorts where the payload types are passed to the Menu

heavy nacelle
#

I would recommend against that. You know which menus are where at compile time. You do not need to make it so all possible overloads are available and you have to check at runtime which ones you can call.

#

I think the key here is to not force a uniform type on your menus. Your menus have different types and behave differently. Squishing them into a single interface will only cause pain.

neat light
#

depending on the exact setup, I wouldn't mind it much, cause it's just simpler

#

I still don't fully get the intended usage or why the callbacks need different pieces of information

#

and how awkward it is/isn't to squeeze things into one uniform interface

#

if you can squeeze thing in one interface, looks like the observer thing he linked just uses polymorphism

heavy nacelle
#

There are some things you can still do, like have a single templated function that does "react to button press on this menu" even though menus are differently typed, but virtual functions are not good for that here.

neat light
#

well in what way

#

unless you force knowledge of all the concrete types at compile-time, you pretty much need some form of polymorphism/virtual dispatch/conversion to a callback which isn't really better than virtual dispatch cause you still eat the pointer indirection anyway

heavy nacelle
#

Just force the knowledge of all the concrete types at compile-time 🤷

neat light
#

I guess avoiding polymorphic lifetime handling, but I reckon you'd do that either way

heavy nacelle
#

It's an embedded system with a static not so big list of menu screens. You can list them all in a single file.

neat light
#

probably

#

I'm just not confident in pushing op down that route now

neat light
#

even just the iteration through the actions is simpler via some form of polymorphism (and I include variant in that)

hollow osprey
#

I think I'll just go with an easier approach of having all the lambdas accept the same type of payload. Maybe I won't need anything too custom. My fear is that I end up with a bloated payload that is created for all gui elements.

heavy nacelle
#

That is indeed the risk. And polymorphism doesn't save you.

neat light
#

my hosted brain is too smooth to understand the implication
are we talking about having too many types of payload?

#

wait no, you're talking about having some kinda of universal payload that can handle any and all gui element payload?

neat light
#

I'm going to be honest and admit I'm not even sure why a gui element would update an "observer" with a different payload type

#

for each observer

heavy nacelle
#

The delete button updates the list of displayed elements. The back button changes the current screen. It happens 🤷

neat light
heavy nacelle
neat light
#

like my question/remark is more about who observes/is observed and payload information needs to passed between the two

heavy nacelle
#

It comes down to "How do I make the button click callback flexible enough to change the current screen and the elements in menu x and [list of more things] without capturing state?"

#

I think "observing" is not the right model here. In theory it's a simple button with an on_click function. In practice the interface is a huge pain.

#

Oh, and repeat this for other GUI elements like checkboxes and menu positions and such.

neat light
#

then the payload isn't really passed through the callback

#

if it's about state, the question is whose state you need to track and pass around

#

if the button interaction is "on click", you call all the callbacks when a click happen, and the callbacks can each have their own dedicated state/user data, but that's not really custom data that the button needs to pass in

#

it's the callback c-style state/user data

#

which has to be stored alongside the callback

#

or referenced alongside the callback

neat light
#

if it's provided by the gui element itself, all the callbacks should kinda expect to receive it when they are invoked

neat light
#

or at least that's my expectation

#

I don't really do GUI, but event-driven stuff with callbacks ime have a more uniform interface

#

at least for the one "entity/element" that would issue the callbacks

hollow osprey
#

It's not a straightforward callback mechanism, because the MenuActions need to draw something all the time. Here's a function that gets called every 10ms:

void Melseq::update()
{
    auto t1 = activeTrackIndex_ == 0 ? MenuActionState::Active : MenuActionState::Default;
    auto t2 = activeTrackIndex_ == 1 ? MenuActionState::Active : MenuActionState::Default;
    auto t3 = activeTrackIndex_ == 2 ? MenuActionState::Active : MenuActionState::Default;
    auto t4 = activeTrackIndex_ == 3 ? MenuActionState::Active : MenuActionState::Default;

    mainMenu_.update(MenuActionId::TrackSelect, MenuActionPayload{Vec2{4, 2}, Vec2{1, 1}, "T1", t1});
    mainMenu_.update(MenuActionId::TrackSelect, MenuActionPayload{Vec2{5, 2}, Vec2{1, 1}, "T2", t2});
    mainMenu_.update(MenuActionId::TrackSelect, MenuActionPayload{Vec2{4, 3}, Vec2{1, 1}, "T3", t3});
    mainMenu_.update(MenuActionId::TrackSelect, MenuActionPayload{Vec2{5, 3}, Vec2{1, 1}, "T4", t4});

    mainMenu_.update(MenuActionId::TapTempo, MenuActionPayload{});
}

But what is drawn is dependent on the "trigger" functions such as:

void Melseq::triggerTapTempo(bool pressed)
{
    mainMenu_.triggerAction(MenuActionId::TapTempo, pressed ? MenuActionState::Active : MenuActionState::Default);
}
#

But the issue is the Payload makes sense for this page, but not for another page that might use a different layout. In the end I'll just add a template param to the Menu and MenuAction class that defines what a Payload should look like for each different Page.

#

solved!

#

!solved