用std::function实现通用事件回调
2024-03-17 17:33:41

最近在实现一个回调功能时遇到一点小难题。基于 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)
{
// 编译错误:error C2678: 二进制“==”: 没有找到接受“MsgCallback”类型的左操作数的运算符(或没有可接受的转换)
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的定义不同,但可以编译通过

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/

上一页
2024-03-17 17:33:41
下一页