在C++中缺少原生反射支持,但我们可利用模板元编程在编译期完成类成员函数的自动注册与绑定,从而实现按名称调用。核心思路是把不同签名的成员函数统一擦除类型,存入全局映射表。

方案整体设计
我们需要解决三个问题:如何获取成员函数地址、如何统一存储、如何按名字调用。下面分模块说明。
1. 类型擦除包装
使用std::function把成员函数转成可调用对象,并通过基类指针隐藏真实类型。
#include <iostream>
#include <string>
#include <map>
#include <functional>
#include <vector>
// 抽象基类,用于擦除类型
struct CallableBase {
virtual ~CallableBase() {}
virtual void call(const std::vector<std::string>& args) = 0;
};
// 针对具体类与成员函数特化
template<typename T, typename R, typename... Args>
struct Callable : CallableBase {
using Fn = R (T::*)(Args...);
Fn fn;
T* obj;
Callable(T* o, Fn f) : obj(o), fn(f) {}
void call(const std::vector<std::string>& args) override {
// 简化示例:假设参数为int,实际可用辅助转换
(obj->*fn)(std::stoi(args[0]));
}
};
2. 全局注册表
用单例map保存名字到CallableBase的映射。
struct Registry {
static std::map<std::string, CallableBase*>& get() {
static std::map<std::string, CallableBase*> m;
return m;
}
};
// 注册辅助函数
template<typename T, typename R, typename... Args>
void register_func(const std::string& name, T* obj, R (T::*f)(Args...)) {
Registry::get()[name] = new Callable<T, R, Args...>(obj, f);
}
3. 示例类与自动绑定
定义业务类,并在构造或初始化时注册成员函数。
class Service {
public:
void hello(int v) {
std::cout << "hello called with " << v << std::endl;
}
void init() {
register_func("hello", this, &Service::hello);
}
};
int main() {
Service s;
s.init();
// 通过名字调用
auto it = Registry::get().find("hello");
if (it != Registry::get().end()) {
it->second->call({"42"});
}
return 0;
}
编译期优化思路
上述注册在运行时完成,若希望完全编译期收集,可用模板静态变量把注册动作放到全局初始化阶段,避免手动init调用。
静态自动注册
template<typename T, typename R, typename... Args>
struct AutoReg {
AutoReg(const std::string& name, T* obj, R (T::*f)(Args...)) {
register_func(name, obj, f);
}
};
// 在类内定义静态实例触发注册
class Service2 {
public:
void run(int x) { std::cout << "run " << x << std::endl; }
static Service2 inst;
static AutoReg<Service2, void, int> reg;
};
Service2 Service2::inst;
AutoReg<Service2, void, int> Service2::reg("run", &Service2::inst, &Service2::run);
注意事项
- 参数转换需自行处理,可结合<code>std::any</code>提升灵活性
- 成员函数指针类型必须精确匹配,模板推导可减轻负担
- 对象生命周期要长于调用时刻,避免悬空指针
该方案在RPC命令分发、脚本桥接中非常实用,扩展新命令只需增加注册行。