34. 如何实现一个支持任意参数转发的委托/回调系统?
这个问题,说白了就是:你写了一个回调容器,怎么让调用方传任意类型、任意数量的参数进去,还能完美转发到真正的处理函数上?
我在项目中遇到过好几次这种需求。最典型的是事件总线——你注册一个回调,它可能接受 int,也可能接受 string,甚至可能接受一个自定义结构体。如果每个回调都写死参数类型,那代码就没法看了。
核心思路:std::function + 可变参数模板 + 完美转发
实现一个通用的委托系统,需要三样东西:
- std::function:存储可调用对象
- 可变参数模板:接受任意数量和类型的参数
- std::forward:保持参数的左值/右值属性
嗯,这里要注意:std::function 本身已经支持了类型擦除,但它需要你在模板参数里声明签名。我们要做的,就是把这个签名也变成模板参数的一部分。
基础实现:一个简单的委托类
#include <functional>
#include <memory>
#include <vector>
template<typename... Args>
class Delegate {
public:
using Callback = std::function<void(Args...)>;
// 注册回调
template<typename F>
void connect(F&& f) {
callbacks_.emplace_back(std::forward<F>(f));
}
// 触发所有回调,完美转发参数
template<typename... Ts>
void invoke(Ts&&... args) {
for (auto& cb : callbacks_) {
cb(std::forward<Ts>(args)...);
}
}
// 清空
void clear() { callbacks_.clear(); }
private:
std::vector<Callback> callbacks_;
};
你看,invoke 里用了 std::forward<Ts>(args)...,这就是完美转发。不管调用方传的是左值还是右值,都能原样传递到回调函数里。
关键点:Delegate<int, std::string> 表示这个委托接受两个参数:int 和 string。但 invoke 的模板参数 Ts 是独立推导的,所以可以完美转发。
使用示例
void foo(int x, const std::string& s) {
std::cout << x << " " << s << std::endl;
}
struct Bar {
void operator()(int x, std::string s) const {
std::cout << "Bar: " << x << " " << s << std::endl;
}
};
int main() {
Delegate<int, std::string> delegate;
// 注册普通函数
delegate.connect(foo);
// 注册 lambda
delegate.connect([](int a, const std::string& b) {
std::cout << "lambda: " << a << " " << b << std::endl;
});
// 注册函数对象
Bar bar;
delegate.connect(bar);
// 触发所有回调
delegate.invoke(42, "hello");
}
输出结果:
42 hello
lambda: 42 hello
Bar: 42 hello
进阶:支持返回值
有时候你需要回调返回结果。比如一个事件过滤器,回调返回 true 表示事件已处理,false 表示继续传递。
template<typename R, typename... Args>
class DelegateWithReturn {
public:
using Callback = std::function<R(Args...)>;
template<typename F>
void connect(F&& f) {
callbacks_.emplace_back(std::forward<F>(f));
}
// 返回最后一个回调的结果
template<typename... Ts>
R invoke(Ts&&... args) {
R result{};
for (auto& cb : callbacks_) {
result = cb(std::forward<Ts>(args)...);
}
return result;
}
private:
std::vector<Callback> callbacks_;
};
小技巧:如果你需要收集所有返回值,可以改成 std::vector<R> 返回。我个人习惯用 std::optional<R> 表示「至少有一个回调返回了有效值」。
再进阶:支持任意参数类型的委托容器
上面那个 Delegate<int, std::string> 是固定签名的。如果你想要一个容器,里面可以放不同签名的委托呢?
这就需要用类型擦除了。我常用的做法是:
class AnyDelegate {
public:
template<typename... Args>
void connect(std::function<void(Args...)> f) {
// 用 lambda 捕获函数,擦除参数类型
callbacks_.emplace_back([f = std::move(f)](const std::any&... args) {
// 这里需要把 any 转回原始类型
// 但实际项目中,我建议用 variant 或自定义类型标签
});
}
template<typename... Args>
void invoke(Args&&... args) {
for (auto& cb : callbacks_) {
cb(std::forward<Args>(args)...);
}
}
private:
std::vector<std::function<void(const std::any&...)>> callbacks_;
};
注意:用 std::any 做类型擦除有性能开销,而且类型转换容易出错。我曾经在一个项目里这么干过,结果调试了整整两天,最后发现是 any_cast 的类型对不上。后来我改用 std::variant + 访问者模式,才彻底解决。
实战中的避坑指南
我曾经在写一个游戏引擎的事件系统时,踩过几个坑:
- 生命周期问题:回调里捕获了
this指针,但对象已经析构了。解决方案是用std::weak_ptr或智能指针管理回调的上下文。 - 递归调用:回调里又触发了同一个委托,导致无限递归。我加了一个递归深度计数器,超过阈值就抛异常。
- 移动语义的坑:如果回调参数是
std::unique_ptr,只能被移动一次。多个回调共享同一个 unique_ptr 会编译失败。这时候需要用std::shared_ptr或者重新设计接口。
知识体系图
总结
实现一个支持任意参数转发的委托系统,核心就是三句话:
- 用
std::function做类型擦除,存储可调用对象 - 用可变参数模板接受任意参数
- 用
std::forward保持参数的原始类型
嗯,其实标准库里的 std::bind 和 std::thread 也是类似的思路。你想想看,std::thread 的构造函数不就是完美转发的典型应用吗?
我个人建议,在实际项目中,如果只是简单的回调,直接用 std::function 就够了。但如果需要复杂的多播委托、生命周期管理、异步调用,那还是得自己封装一层。毕竟,标准库只提供了基础组件,真正的架构设计还是要靠我们自己。
一句话总结:完美转发让委托系统既能保持类型安全,又能零开销地传递任意参数。这是 C++11 之后写回调系统的标准姿势。
公众号:蓝海资料掘金营,微信deep3321