1

I am going from C development to C++ on the STM32 platform and simply cant find a suitable solution for my problem.

Please have a look at the simplified example code attached to this post.

#include <iostream>
#include <functional>
#include <list>

using namespace std;

class Pipeline {
public:
    std::list<std::function<void(Pipeline*)>> handlers;
    
    //add handler to list --> works fine
    void addHandler(std::function<void(Pipeline*)> handler) {
        this->handlers.push_front(handler);
    }
    
    void ethernetCallback(void) {
        //handle received data and notify all callback subscriptions --> still works fine
        // this callback function is normally sitting in a child class of Pipeline
        int len = handlers.size();
        for (auto const &handler : this->handlers) {
            handler(this);
        }
    }
    
    void removeHandler(std::function<void(Pipeline*)> handler) {
        // Here starts the problem. I can not use handlers.remove(handler) here to
        // unregister the callback function. I understood why I can't do that,
        // but I don't know another way of coding the given situation.
    }
};

class Engine {
public: 
    void callback(Pipeline *p) {
        // Gets called when new data arrives 
        cout<<"I've been called.";
    }
    void assignPipelineToEngine(Pipeline *p) {
        p->addHandler(std::bind(&Engine::callback, this, std::placeholders::_1));
    }
};

int main()
{
    Engine *e = new Engine();
    Pipeline *p = new Pipeline();
    e->assignPipelineToEngine(p);
    // the ethernet callback function would be called by LWIP if new udp data is available
    // calling from here for demo purposes only
    p->ethernetCallback();

    return 0;
}

The idea is that when the class "Pipeline" receives new data over ethernet, it informs all registered callback functions by calling a method. The callback functions are stored in a std::list. Everything works fine till here, but the problem with this approach is that I can't remove the callback functions from the list, which is required for the project. I know why I can't simply remove the callback function pointers from the list, but I don't know another approach at the moment.

Probably anybody could give me a hint where I could have a look for solving this problem. All resources I've researched don't really show my specific case.

Thank you all in advance for your support! :)

4
  • 1
    Side note: since you are self-admittedly new to C++, make sure you want the linked list-like behaviour defined by std::list and not the array-like behaviour defined by std::vector. In general, unless you add and remove items far more frequently than you iterate the container, vector almost always comes out ahead. As soon as you have to find the item to remove it, as you do here, the number of iterations will at least equal the number of removals and you probably lose list's easy-removal advantage. Commented Oct 11, 2022 at 20:24
  • 1
    Side note: Look into using lambda expressions in place of std::bind. They are often easier to wrangle (p->addHandler([this](Pipeline *p) {callback(p);}); vs p->addHandler(std::bind(&Engine::callback, this, std::placeholders::_1));) and more performant. Commented Oct 11, 2022 at 20:32
  • 1
    When and who would remove the callback? Does the callback itself decide "Okay, I'm done, unregister me"? Does it happen from a different thread? Does it happen while looping over all callbacks or in-between?
    – Homer512
    Commented Oct 11, 2022 at 21:33
  • Just be aware of the dynamic memory behavior of STL containers. You didn't mentioned the model of the STM32 you're using, but with some exceptions like STM32MP series, most of STM32 are microcontroller-level products with limited resources. Frequent usage of dynamic memory may cause problems.
    – Tagli
    Commented Oct 12, 2022 at 6:46

2 Answers 2

1

One option would be to have addHandler return some sort of identifier that can later be passed to removeHandler. For example:

class Pipeline {
public:
    std::map<int, std::function<void(Pipeline*)>> handlers;
    int nextId = 0;
    
    //add handler to list --> works fine
    void addHandler(std::function<void(Pipeline*)> handler) {
        handlers[nextId++] = handler;
    }
    
    void ethernetCallback(void) {
        for (auto const& entry : handlers) {
            entry.second(this);
        }
    }
    
    void removeHandler(int handlerToken) {
        handlers.erase(handlerToken);
    }
};

class Engine {
public:
    void callback(Pipeline *p) {
        // Gets called when new data arrives 
        cout<<"I've been called.";
    }

    void assignPipelineToEngine(Pipeline *p) {
        handlerToken = p->addHandler(
            std::bind(
                &Engine::callback,
                this,
                std::placeholders::_1
            )
        );
    }

    void unregisterPipelineFromEngine(Pipeline *p) {
        p->removeHandler(handlerToken);
    }
private:
    int handlerToken;
};
2
  • Thanks for you answer. I should have mentioned that there can and will be multiple callback handlers. So the idea with the token would not work - correct? Commented Oct 12, 2022 at 9:34
  • @LeonReucher Sure it would, the Engine would just need to keep a collection of tokens; they're just int; it can track them however it wants. Commented Oct 12, 2022 at 15:43
0

Perhaps you could attach an ID to each handler. Very crude variant would just use this address as an ID if you have at most one callback per instance.

#include <functional>
#include <iostream>
#include <list>

using namespace std;

class Pipeline {
   public:
    using ID_t = void *;  // Or use integer-based one...
    struct Handler {
        std::function<void(Pipeline *)> callback;
        ID_t id;
        // Not necessary for emplace_front since C++20 due to agreggate ctor
        // being considered.
        Handler(std::function<void(Pipeline *)> callback, ID_t id)
            : callback(std::move(callback)), id(id) {}
    };
    std::list<Handler> handlers;

    // add handler to list --> works fine
    void addHandler(std::function<void(Pipeline *)> handler, ID_t id) {
        this->handlers.emplace_front(std::move(handler), id);
    }

    void ethernetCallback(void) {
        // handle received data and notify all callback subscriptions --> still
        // works fine
        //  this callback function is normally sitting in a child class of
        //  Pipeline
        int len = handlers.size();
        for (auto const &handler : this->handlers) {
            handler.callback(this);
        }
    }

    void removeHandler(ID_t id) {
        handlers.remove_if([id = id](const Handler &h) { return h.id == id; });
    }
};

class Engine {
   public:
    void callback(Pipeline *p) {
        // Gets called when new data arrives
        cout << "I've been called.";
    }
    void assignPipelineToEngine(Pipeline *p) {
        //p->addHandler(std::bind(&Engine::callback, this, std::placeholders::_1), this);
        //Or with a lambda
        p->addHandler([this](Pipeline*p){this->callback(p);},this);
    }
    void removePipelineFromEngine(Pipeline *p) { p->removeHandler(this); }
};

int main() {
    Engine *e = new Engine();
    Pipeline *p = new Pipeline();
    e->assignPipelineToEngine(p);
    // the ethernet callback function would be called by LWIP if new udp data is
    // available calling from here for demo purposes only
    p->ethernetCallback();

    return 0;
}

You might also consider std::map<ID_t,std::function<...>> instead of list, not sure how memory/performance constrained you are.

Obligatory: do not use new, use std::unique_ptr, or better use automatic storage whenever you can. Although in this case a pointer is appropriate for e as you need stable address due to this capture/bind/ID.

std::functions are not comparable as there isn't a good generic way how to define this comparison.

2
  • Thank you very much for your answer. Could you explain the restriction "if you have at most one callback per instance." to me? I've tested your code with multiple Engines and Pipelines and it seems to work fine. Commented Oct 12, 2022 at 9:48
  • 1
    @LeonReucher It would not work if you had registered from a single Engine object two callbacks. for example duplicating p->addHandler line. Because then there would be two callbacks with the same ID(same this pointer) and removeHandler would delete the first one only.
    – Quimby
    Commented Oct 12, 2022 at 9:57

Not the answer you're looking for? Browse other questions tagged or ask your own question.