C++ 成员回调函数列表

C++ List of member callback functions

提问人:Leon Reucher 提问时间:10/12/2022 更新时间:10/12/2022 访问量:186

问:

我正在从C开发到STM32平台上的C++,根本找不到适合我的问题的解决方案。

请查看本文所附的简化示例代码。

#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;
}

这个想法是,当类“Pipeline”通过以太网接收新数据时,它会通过调用方法通知所有已注册的回调函数。回调函数存储在 std::list 中。到目前为止一切正常,但这种方法的问题是我无法从列表中删除回调函数,这是项目所必需的。 我知道为什么我不能简单地从列表中删除回调函数指针,但目前我不知道另一种方法。

也许任何人都可以给我一个提示,我可以在哪里寻找解决这个问题。我研究过的所有资源都没有真正显示我的具体情况。

提前感谢大家的支持!:)

C++ 事件 回调 STM32

评论

1赞 user4581301 10/12/2022
旁注:由于您自认是 C++ 的新手,因此请确保您希望由 定义的类链表行为,而不是由 定义的类数组行为。通常,除非添加和删除项的频率远远高于迭代容器的频率,否则几乎总是排在前面。一旦你必须找到要删除它的项目,就像你在这里所做的那样,迭代次数将至少等于删除的次数,你可能会失去易于删除的优势。std::liststd::vectorvectorlist
1赞 user4581301 10/12/2022
旁注:考虑使用 lambda 表达式代替 .它们通常更容易争吵(vs)并且性能更高。std::bindp->addHandler([this](Pipeline *p) {callback(p);});p->addHandler(std::bind(&Engine::callback, this, std::placeholders::_1));
1赞 Homer512 10/12/2022
何时以及由谁删除回调?回调本身是否决定“好的,我完成了,注销我”?它是否发生在不同的线程中?它是在循环遍历所有回调时还是在两者之间发生?
0赞 Tagli 10/12/2022
只需注意 STL 容器的动态内存行为即可。您没有提到您正在使用的 STM32 的型号,但除了 STM32MP 系列等一些例外,大多数 STM32 都是资源有限的微控制器级产品。频繁使用动态内存可能会导致问题。

答:

0赞 Quimby 10/12/2022 #1

也许您可以为每个处理程序附加一个 ID。非常粗略的变体将只使用地址作为 ID,如果每个实例最多有一个回调。this

#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;
}

您也可以考虑而不是列出,不确定您的内存/性能受到的限制程度。std::map<ID_t,std::function<...>>

强制性:尽可能不要使用、使用或更好地使用自动存储。尽管在这种情况下,指针是合适的,因为由于捕获/绑定/ID 而需要稳定的地址。newstd::unique_ptrethis

std::functions没有可比性,因为没有一个好的通用方法来定义这种比较。

评论

0赞 Leon Reucher 10/12/2022
非常感谢您的回答。您能向我解释一下“如果每个实例最多有一个回调”的限制吗?我已经使用多个引擎和管道测试了您的代码,它似乎工作正常。
1赞 Quimby 10/12/2022
@LeonReucher 如果您从单个对象注册了两个回调,它将不起作用。例如,复制行。因为这样就会有两个具有相同 ID(相同指针)的回调,并且只会删除第一个回调。Enginep->addHandlerthisremoveHandler
1赞 Miles Budnek 10/12/2022 #2

一种选择是返回某种标识符,该标识符稍后可以传递给 。例如:addHandlerremoveHandler

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;
};

评论

0赞 Leon Reucher 10/12/2022
谢谢你的回答。我应该提到,可以而且将会有多个回调处理程序。所以使用令牌的想法是行不通的 - 对吗?
0赞 Miles Budnek 10/12/2022
@LeonReucher 当然可以,只需要保留一组令牌;他们只是 ;它可以随心所欲地跟踪它们。Engineint