CRTP静态变量未初始化问题:为何手动定义类构造函数才生效?
尝试用CRTP实现支持类型自动注册的工厂,但遇到了一个奇怪的问题:当注释掉类B的构造函数时程序运行失败,手动定义构造函数则正常运行。按道理不管是否手动定义构造函数,类B的默认构造函数都应该调用基类AutoReg<B>的构造函数,但未手动定义时,AutoReg<B>::registered从未初始化,这是为什么?
代码如下:
#include <iostream> #include <map> #include <functional> class A { public: virtual ~A() = default; virtual void junk() = 0; }; class Factory { public: static A* get(const std::string& name) { return map()[name](); } template<typename T> static bool reg() { map()[T::asdf] = [](){ return new T(); }; return true; } static std::map<std::string, std::function<A*()>>& map() { static std::map<std::string, std::function<A*()>> m; return m; } }; template<typename T> class AutoReg : public A { public: AutoReg(){ std::ignore = registered; } static inline bool registered = Factory::reg<T>(); }; class B : public AutoReg<B> { public: //B(){} void junk() override { std::cout << "B" << std::endl; } static const inline std::string asdf = "asdf"; }; int main(int argc, char** argv) { Factory::get("asdf")->junk(); return 0; }
核心原因:惰性模板实例化
当你没有手动定义B的构造函数时,编译器会生成默认构造函数,但这个默认构造函数的实例化是惰性的——只有当代码中实际调用B的构造函数时,编译器才会实例化这个默认构造函数,进而触发基类AutoReg<B>的构造函数,最终触发表静态成员registered的初始化。
但你的代码逻辑形成了死循环:要通过Factory::get创建B对象,需要先在工厂中注册创建B的lambda;要注册lambda,需要AutoReg<B>::registered初始化;要初始化registered,需要调用B的构造函数触发基类构造,但此时工厂里还没有注册创建B的函数,根本无法调用构造函数。
而当你手动定义B的构造函数时,编译器会立即实例化这个构造函数(手动定义的成员函数会被视为需要立即处理),进而触发基类AutoReg<B>的构造函数,让registered完成初始化,将lambda注册到工厂中,后续Factory::get就能正常获取到创建B的函数。
解决方法
打破依赖循环,让静态成员的初始化不依赖于构造函数调用。修改AutoReg模板,利用模板静态成员的初始化特性,确保在程序启动阶段自动完成注册:
template<typename T> class AutoReg : public A { protected: // 基类构造函数保持默认,无需触发registered AutoReg() = default; private: // 借助静态辅助结构体的构造函数触发注册 struct RegisterHelper { RegisterHelper() { Factory::reg<T>(); } }; static inline RegisterHelper helper; };
修改后,只要AutoReg<B>被实例化(因为B继承了它),RegisterHelper类型的静态成员helper就会在程序启动时完成初始化,自动调用Factory::reg<B>()完成注册,不管B有没有手动定义构造函数,都能正常工作。
内容的提问来源于stack exchange,提问作者BadProgrammer99

