如何让std::function引用可调用对象?基于std::function实现发布订阅疑问
我来帮你拆解这个问题,一步步捋清楚哈~
首先看你代码里的场景:当你调用sub.Subscribe(a)时,确实会复制Test对象。因为std::function默认会把传入的可调用对象完整拷贝到自己的内部存储中,这里的a作为Test实例,会被复制一份存到sub.callback_里。
如何让std::function引用可调用对象?
有两种常用方式可以让std::function持有对象的引用而非拷贝,同时要注意对象生命周期的问题:
1. 用std::ref/std::cref包装对象
std::ref(非const引用)或std::cref(const引用)可以把对象包装成引用包装器,让std::function存储这个包装器,从而间接持有原对象的引用:
#include <functional> #include <iostream> using Callback = std::function<void ()>; struct Test { void operator()() { std::cout << a << std::endl; }; int a; }; struct Sub { void Subscribe(Callback callback) { callback_ = callback; } void trigger() { callback_(); } Callback callback_; }; int main() { Test a = {1}; Sub sub; // 用std::ref包装a,让std::function持有引用 sub.Subscribe(std::ref(a)); a.a = 2; // 修改原对象的值 sub.trigger(); // 会输出2,说明引用生效 return 0; }
⚠️ 重点提醒:必须保证被引用的对象a生命周期长于sub,否则sub.callback_会变成悬空引用,调用时触发未定义行为。
2. 用lambda捕获引用
可以通过lambda表达式捕获Test对象的引用,再把lambda传给std::function,lambda内部会直接引用原对象:
int main() { Test a = {1}; Sub sub; // 捕获a的引用 sub.Subscribe([&a]() { a(); }); a.a = 2; sub.trigger(); // 输出2 return 0; }
同样要注意:lambda捕获的引用不能在原对象销毁后使用,否则会出问题。
其他实现建议
1. 支持多订阅者
你的当前实现只能存储一个回调,实际发布订阅模式通常需要支持多个订阅者。可以把callback_改成容器(比如std::vector<Callback>),让Subscribe负责添加回调,trigger遍历调用所有回调:
struct Sub { void Subscribe(Callback callback) { callbacks_.push_back(std::move(callback)); } void trigger() { for (auto& cb : callbacks_) { cb(); } } std::vector<Callback> callbacks_; };
用std::move可以避免不必要的拷贝,提升效率。
2. 模板替代std::function(性能优化)
如果不需要类型擦除的灵活性,可用模板定义Subscribe方法,避免std::function的运行时开销:
struct Sub { template<typename Func> void Subscribe(Func&& func) { callback_ = std::forward<Func>(func); } void trigger() { callback_(); } std::function<void()> callback_; }; // 更极端的情况:如果不需要存储不同类型的回调,甚至可以直接存储模板类型 template<typename Func> struct SubTemplate { SubTemplate(Func&& func) : callback_(std::forward<Func>(func)) {} void trigger() { callback_(); } Func callback_; };
这种方式在性能敏感场景更优,但牺牲了类型擦除的灵活性。
3. 智能指针管理生命周期
如果担心对象生命周期不确定,可用std::shared_ptr管理Test对象,让std::function持有shared_ptr,只要有回调存在,对象就不会被销毁:
int main() { auto a = std::make_shared<Test>(); a->a = 1; Sub sub; sub.Subscribe([a]() { (*a)(); }); a->a = 2; sub.trigger(); // 输出2 return 0; }
这种方式适合对象生命周期难以把控的场景,彻底避免悬空引用。
4. 线程安全优化
如果发布订阅在多线程环境使用,要给订阅、触发操作加互斥锁,避免竞态条件:
#include <mutex> struct Sub { void Subscribe(Callback callback) { std::lock_guard<std::mutex> lock(mtx_); callbacks_.push_back(std::move(callback)); } void trigger() { std::lock_guard<std::mutex> lock(mtx_); for (auto& cb : callbacks_) { cb(); } } std::vector<Callback> callbacks_; std::mutex mtx_; };
内容的提问来源于stack exchange,提问作者Bryan Fok

