不使用多态与std::function存储不同类型调用者的回调容器的实现方法咨询
我完全懂你现在的困境:想让Manager存储不同类的成员函数回调,但又不想用多态或者std::function,试了普通函数指针却因为成员函数指针的类型差异直接卡壳了。先给你掰扯清楚为啥原来的代码会报错:普通的函数指针void(*)()和类的非静态成员函数指针void (Caller_N::*)()是完全不同的类型——成员函数指针本质上需要绑定一个类的实例才能被调用,它的类型里天生带着所属类的信息,所以根本没法直接转成普通函数指针,这就是编译器给你抛错的核心原因。
接下来给你几个满足“不管多脏”要求的可行方案,全都是绕开多态和std::function的思路:
方案1:C风格回调(void*上下文 + 静态包装函数)
这是最经典也最稳妥的“手动绑定”方案,本质上就是自己实现std::function底层做的类型擦除,完全没用到多态或标准库的函数包装器:
#include <map> // 定义统一的回调签名:带void*类型的上下文参数 using FuncPtr = void(*)(void*); struct Manager { void add_callback(int key, FuncPtr ptr, void* context) { callbacks[key] = {ptr, context}; } // 示例:根据key调用对应的回调 void invoke(int key) { auto& entry = callbacks.at(key); entry.func(entry.context); } private: // 把函数指针和对应的对象实例打包存储 struct CallbackEntry { FuncPtr func; void* context; }; std::map<int, CallbackEntry> callbacks; }; Manager m; struct Caller_1 { void x() { // 用静态包装函数适配成普通函数指针,把当前对象实例this作为上下文传进去 m.add_callback(1, &Caller_1::callback_wrapper, this); } void callback() { // 这里写你的实际回调逻辑 } private: // 静态包装函数:把void*转成Caller_1指针,再调用成员函数 static void callback_wrapper(void* context) { static_cast<Caller_1*>(context)->callback(); } }; struct Caller_2 { void x() { m.add_callback(2, &Caller_2::callback_wrapper, this); } void callback() { // 这里写你的实际回调逻辑 } private: static void callback_wrapper(void* context) { static_cast<Caller_2*>(context)->callback(); } }; int main() { Caller_1 c1; Caller_2 c2; c1.x(); c2.x(); // 测试调用回调 m.invoke(1); m.invoke(2); return 0; }
这个方案的核心是用静态成员函数当“适配器”,它的类型符合普通函数指针要求,再通过void*传递对象实例,在适配器里转成具体类的指针后调用成员函数。兼容性拉满,可维护性也比其他“脏方案”好得多。
方案2:手动类型擦除(联合体存储成员函数指针)
如果不想写静态包装函数,可以用联合体存储不同类型的成员函数指针,再配合一个“调用器”函数来处理不同类型的调用逻辑,这个方案更“底层”一点:
#include <map> // 提前声明所有可能的回调所属类 struct Caller_1; struct Caller_2; // 定义每种成员函数的类型 using Caller1MemFn = void(Caller_1::*)(); using Caller2MemFn = void(Caller_2::*)(); // 用联合体存不同类型的成员函数指针 union MemFnUnion { Caller1MemFn c1_fn; Caller2MemFn c2_fn; // 以后加新的Caller类型,就在这里加对应的成员函数指针类型 }; // 统一的调用器签名:接收对象上下文和成员函数联合体 using InvokerFn = void(*)(void* context, const MemFnUnion& fn); struct Manager { struct CallbackEntry { InvokerFn invoker; void* context; MemFnUnion fn; }; void add_callback(int key, InvokerFn invoker, void* context, const MemFnUnion& fn) { callbacks[key] = {invoker, context, fn}; } void invoke(int key) { auto& entry = callbacks.at(key); entry.invoker(entry.context, entry.fn); } private: std::map<int, CallbackEntry> callbacks; }; Manager m; struct Caller_1 { void x() { MemFnUnion fn; fn.c1_fn = &Caller_1::callback; m.add_callback(1, &caller1_invoker, this, fn); } void callback() {} }; struct Caller_2 { void x() { MemFnUnion fn; fn.c2_fn = &Caller_2::callback; m.add_callback(2, &caller2_invoker, this, fn); } void callback() {} }; // 针对Caller_1的专属调用器 void caller1_invoker(void* context, const MemFnUnion& fn) { (static_cast<Caller_1*>(context)->*(fn.c1_fn))(); } // 针对Caller_2的专属调用器 void caller2_invoker(void* context, const MemFnUnion& fn) { (static_cast<Caller_2*>(context)->*(fn.c2_fn))(); } int main() { Caller_1 c1; Caller_2 c2; c1.x(); c2.x(); m.invoke(1); m.invoke(2); return 0; }
这个方案的缺点是扩展性差——每加一个新的Caller类型,就得修改联合体、新增对应的调用器函数,但胜在完全没用到标准库的包装器或多态。
方案3:强制类型转换硬刚(极端脏方案,不推荐)
如果你的场景极端到连静态包装函数都不想写,可以试试靠reinterpret_cast强制转类型,这属于C++标准里的未定义行为,完全依赖编译器和平台的成员函数指针布局,很容易崩,但确实能“跑”:
#include <map> struct Manager { enum class CallerType { C1, C2 }; struct CallbackEntry { CallerType type; void* fn_ptr; void* context; }; void add_callback(int key, CallerType type, void* fn_ptr, void* context) { callbacks[key] = {type, fn_ptr, context}; } void invoke(int key) { auto& entry = callbacks.at(key); switch(entry.type) { case CallerType::C1: { auto mem_fn = reinterpret_cast<void(Caller_1::*)()>(entry.fn_ptr); (static_cast<Caller_1*>(entry.context)->*mem_fn)(); break; } case CallerType::C2: { auto mem_fn = reinterpret_cast<void(Caller_2::*)()>(entry.fn_ptr); (static_cast<Caller_2*>(entry.context)->*mem_fn)(); break; } } } private: std::map<int, CallbackEntry> callbacks; }; Manager m; struct Caller_1 { void x() { m.add_callback(1, Manager::CallerType::C1, reinterpret_cast<void*>(&Caller_1::callback), this); } void callback() {} }; struct Caller_2 { void x() { m.add_callback(2, Manager::CallerType::C2, reinterpret_cast<void*>(&Caller_2::callback), this); } void callback() {} };
这个方案完全是靠“硬转”绕过类型检查,不同编译器的成员函数指针布局可能天差地别,换个环境大概率崩,只适合极端场景。
最后给你个小提醒:如果不是有特别强的性能需求或者环境限制,std::function其实已经封装得非常好了,它的底层逻辑和方案1类似,但帮你处理了所有细节,不用自己写容易出错的代码。不过既然你明确说不想用,方案1是最稳妥的“脏方案”,兼顾兼容性和可维护性。
内容来源于stack exchange

