You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

不使用多态与std::function存储不同类型调用者的回调容器的实现方法咨询

不使用多态与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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.07 12:52:59