最近在实现一个回调功能时遇到一点小难题。基于 std::function
实现的回调,它没有 ==
操作,无法比较也就无法取消订阅,查阅了些资料后总结出主流的几种解决方案
如何从 vector 中移除 function 先看看问题代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 #include <iostream> #include <iomanip> #include <functional> #include <vector> class Person { public : void cb1 () { std::cout << "cb1" << std::endl; } void cb2 () { std::cout << "cb2" << std::endl; } }; using MsgCallback = std::function<void (void )>;class Manager { public : void subscribe (const MsgCallback& cb) { _list.push_back (cb); } void unsubscribe (MsgCallback& cb) { std::erase_if (_list, [&](const MsgCallback& elem) {return cb == elem; }); } private : std::vector<MsgCallback> _list; }; int main () { Manager mgr; Person obj{}; auto callback = std::bind (&Person::cb1, &obj); mgr.subscribe (callback); return 0 ; }
原因就在于 std::function
没有 ==
操作符,不可以进行比较,如果不比较就没法取消事件订阅。 目前我认为可用的三种解决方案
订阅时提供一个Key 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 class Manager { public : void subscribe (const std::string& key, const MsgCallback& cb) { } void unsubscribe (const std::string& key) { } private : std::map<std::string, MsgCallback> _list; };
订阅者需要提供一个唯一的 key,用于取消订阅时使用。
用 std::weak_ptr 1 2 3 4 5 6 7 8 9 10 11 class Manager { public : void subscribe (const std::shared_ptr<MsgCallback>& cb) { _list.push_back (cb); } private : std::vector<std::weak_ptr<MsgCallback>> _list; };
这样就不再需要 unsubscribe
方法了,订阅者只需要将指针置空即可自动取消。 触发事件时应该检查指针是否为空,失效的指针应当即时清理。
用 vector 的索引 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 std::vector<std::function<void ()> subs; std::size_t subscribe(std::function<void ()> f) { if (auto it = std::find(subs.begin(), subs.end(), nullptr ); it != subs.end()) { *it = f; return std::distance(subs.begin(), it); } else { subs.push_back(f); return subs.size() - 1; } } void unsubscribe(std::size_t index){ subs[index] = nullptr ; }
和第一种方案类似,订阅者需要持有一个 key,不过这个 key 就是索引而已。我个人更喜欢这种方案。
通用的事件管理器 最后通过上面提到的第三种方式实现一个通用的事件类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 template <typename ... Args>class Event final { public : using Handler = std::function<void (Args...)>; Event () = default ; Event (const Event&) = delete ; Event (Event&& other) noexcept { move_from (other); } std::size_t subscribe (const Handler& f) { if (auto it = std::find (_subscribers.begin (), _subscribers.end (), nullptr ); it != _subscribers.end ()) { *it = f; return std::distance (_subscribers.begin (), it); } _subscribers.push_back (f); return _subscribers.size () - 1 ; } void unsubscribe (std::size_t index) { if (index < _subscribers.size ()) _subscribers[index] = nullptr ; } template <typename ... TriggerArgs> void trigger (TriggerArgs&&... args) const { static_assert (sizeof ...(TriggerArgs) == sizeof ...(Args), "Number of trigger arguments does not match event arguments." ); for (const auto & subscriber : _subscribers) { if (subscriber != nullptr ) subscriber (std::forward<TriggerArgs>(args)...); } } Event& operator =(Event&) = delete ; Event& operator =(Event&& other) noexcept { if (this != &other) move_from (other); return *this ; } private : void move_from (Event& other) { _subscribers = std::move (other._subscribers); } std::vector<Handler> _subscribers; };
唯一的瑕疵是使用 subscribe
时可以接受不同签名的函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 int main () { Event<int > event; Person obj{}; const auto cb1 = std::bind (&Person::cb1, &obj, std::placeholders::_1); const auto cb2 = std::bind (&Person::cb2, &obj); event.subscribe (cb1); event.subscribe (cb2); event.trigger (42 ); return 0 ; }
这是因为 std::function<void(Args...)>
可以接受更少的参数,只要没有被使用即可。编译器不会报错,因为在调用 cb2
时不会传递任何实参,因此没有参数被使用。
参考 https://stackoverflow.com/questions/39633596/ https://stackoverflow.com/questions/47249465/ https://stackoverflow.com/questions/74089196/