如何解决C++脚本引擎基类中泛型createClass方法无法重写的问题?
这个问题简直戳中了C++多态与模板结合的痛点——虚函数没法模板化,但你又需要不同脚本引擎子类对特定类型做定制化的类绑定逻辑。我之前做脚本绑定框架时也遇到过几乎一模一样的问题,下面给你几个比类型擦除高效得多的解决方案:
方案1:非模板虚接口 + 子类模板实现
核心思路是把模板逻辑从虚函数中剥离,基类提供一个非模板的虚接口,子类用模板方法实现具体的类型绑定逻辑,既保留动态多态的灵活性,又能利用模板的类型安全。
步骤1:定义通用的脚本类包装基类
首先创建一个非模板的抽象基类,作为所有脚本绑定类的公共接口:
class ScriptClassWrapper { public: virtual ~ScriptClassWrapper() = default; // 通用的方法绑定接口(可根据需求扩展) virtual void bindMethod(const std::string& name, void* methodPtr, const std::type_info& methodType) = 0; };
步骤2:修改ScriptEng基类
把原来的模板createClass拆分为对外的模板包装函数和子类需要实现的非模板虚函数:
#include <typeinfo> class ScriptEng { public: ScriptEng() = default; virtual ~ScriptEng() = default; // 对外友好的模板接口,方便调用者使用 template<class T> ScriptClass<T>* createClass(const std::string& name, T* instance) { auto wrapper = createClassImpl(typeid(T), name, static_cast<void*>(instance)); // 安全转换为具体的ScriptClass<T>,子类必须保证返回对应类型的实例 return static_cast<ScriptClass<T>*>(wrapper); } virtual bool callFunction(const std::string& name) { return true; } protected: // 子类需要实现的非模板虚接口,传递类型信息和无类型指针 virtual ScriptClassWrapper* createClassImpl(const std::type_info& typeInfo, const std::string& name, void* instance) = 0; };
步骤3:在LuaScriptEng中实现定制逻辑
子类通过模板方法处理不同类型的绑定,还可以对特定类型做特化优化:
class LuaScriptEng : public ScriptEng { protected: ScriptClassWrapper* createClassImpl(const std::type_info& typeInfo, const std::string& name, void* instance) override { // 转发到模板实现函数 return createClassTemplate(typeInfo, name, instance); } private: // 通用模板实现,处理大部分类型的Lua绑定 template<class T> LuaScriptClass<T>* createClassTemplate(const std::type_info&, const std::string& name, void* instance) { T* typedInstance = static_cast<T*>(instance); // 这里写Lua专属的绑定逻辑:创建userdata、注册元表、设置垃圾回收回调等 auto luaClass = new LuaScriptClass<T>(name, typedInstance); return luaClass; } // 对Game类做特化,提前绑定通用方法或做Lua专属优化 template<> LuaScriptClass<Game>* createClassTemplate<Game>(const std::type_info&, const std::string& name, void* instance) { Game* game = static_cast<Game*>(instance); auto luaGameClass = new LuaScriptClass<Game>(name, game); // 提前绑定hello方法,避免调用者重复写 luaGameClass->bindMethod("hello", &Game::hello); return luaGameClass; } };
这个方案的性能开销极小:只有一次虚函数调用,后续的模板逻辑都是编译时确定的,完全没有类型擦除的多层调用开销。
方案2:动态注册类型工厂函数
如果不想修改基类的虚函数结构,可以用类型映射表来注册不同类型的创建函数,灵活性更高,还能支持动态添加类型。
修改ScriptEng基类
添加一个类型到创建函数的映射表,对外提供注册接口和模板化的createClass:
#include <typeindex> #include <unordered_map> #include <functional> class ScriptEng { public: using ClassCreateFunc = std::function<ScriptClassWrapper*(const std::string&, void*)>; ScriptEng() = default; virtual ~ScriptEng() = default; // 注册特定类型的创建函数 template<class T> void registerClassCreator(ClassCreateFunc func) { _creators[type_index(typeid(T))] = func; } // 对外的模板调用接口 template<class T> ScriptClass<T>* createClass(const std::string& name, T* instance) { auto it = _creators.find(type_index(typeid(T))); if (it != _creators.end()) { auto wrapper = it->second(name, static_cast<void*>(instance)); return static_cast<ScriptClass<T>*>(wrapper); } // 未注册时返回默认实现,或抛出异常 return new ScriptClass<T>(instance); } virtual bool callFunction(const std::string& name) { return true; } private: std::unordered_map<std::type_index, ClassCreateFunc> _creators; };
在LuaScriptEng中注册创建函数
在子类构造函数中注册各个类型的Lua绑定逻辑:
class LuaScriptEng : public ScriptEng { public: LuaScriptEng() { // 注册Game类的Lua绑定函数 registerClassCreator<Game>([](const std::string& name, void* instance) { Game* game = static_cast<Game*>(instance); auto luaClass = new LuaScriptClass<Game>(name, game); luaClass->bindMethod("hello", &Game::hello); return luaClass; }); // 注册其他类型... registerClassCreator<Player>([](const std::string& name, void* instance) { return new LuaScriptClass<Player>(name, static_cast<Player*>(instance)); }); } };
这个方案的性能开销主要是一次哈希表查找(unordered_map的查找时间接近O(1)),然后是一次函数调用,比类型擦除高效得多,而且不需要修改基类的虚函数结构,扩展性更强。
方案对比
- 方案1更适合类型固定、需要在子类中集中处理绑定逻辑的场景,代码结构更清晰,性能略优。
- 方案2更适合需要动态添加类型、或者不想修改基类虚函数的场景,灵活性更高。
两种方案都能完美支持你想要的脚本调用形式(比如Game.somefunction()),因为子类的LuaScriptClass<T>可以实现对应的脚本语言绑定逻辑(比如Lua中把方法注册到元表,让脚本可以用面向对象的方式调用)。
内容的提问来源于stack exchange,提问作者Steven Venham

