CRTP静态模板成员初始化问题:默认与显式构造函数差异
我尝试实现一个CRTP自动注册的工厂类,但遇到了静态模板成员初始化的奇怪问题。测试代码如下:
#include <string> #include <unordered_map> #include <iostream> #include <memory> #include <functional> #include <string_view> template <typename Base> class Factory { template <typename, typename> friend class Registrable; private: static std::unordered_map<std::string, std::shared_ptr<Base>> &map() { static std::unordered_map<std::string, std::shared_ptr<Base>> map; return map; } template <typename Derived> static void subscribe(std::string name) { // insert already-exist-check here std::cout << "registered: " << name << std::endl; map().emplace(std::move(name), std::static_pointer_cast<Base>(std::make_shared<Derived>(Derived()))); } }; template <typename T> constexpr auto type_name() noexcept { std::string_view name, prefix, suffix; #ifdef __clang__ name = __PRETTY_FUNCTION__; prefix = "auto type_name() [T = "; suffix = "]"; #elif defined(__GNUC__) name = __PRETTY_FUNCTION__; prefix = "constexpr auto type_name() [with T = "; suffix = "]"; #endif name.remove_prefix(prefix.size()); name.remove_suffix(suffix.size()); return name; } template <typename Base, typename Derived> class Registrable { protected: Registrable() { isRegistered = true; } ~Registrable() = default; static bool init() { Factory<Base>::template subscribe<Derived>(std::string(type_name<Derived>())); return true; } private: static bool isRegistered; }; template <typename Base, typename Derived> bool Registrable<Base, Derived>::isRegistered = Registrable<Base, Derived>::init(); struct MyFactoryBase { virtual ~MyFactoryBase() = default; virtual void method() const = 0; }; struct MyFactory1 : public MyFactoryBase, public Registrable<MyFactoryBase, MyFactory1> { void method() const override { std::cout << "yay Class1" << std::endl; } MyFactory1() = default; }; struct MyFactory2 : public MyFactoryBase, public Registrable<MyFactoryBase, MyFactory2> { void method() const override { std::cout << "yay Class1" << std::endl; } MyFactory2() : MyFactoryBase(), Registrable<MyFactoryBase, MyFactory2>() {} }; int main() { return 0; }
编译环境:gcc (GCC) 8.3.1 20191121 (Red Hat 8.3.1-5)
程序输出:
registered: MyFactory2
核心疑问:为何MyFactory2能自动注册而MyFactory1不能?默认构造函数与显式编写的近乎默认构造函数之间有何差异?
核心原因:模板静态成员的实例化触发规则差异
C++标准中,类模板的静态成员不会自动实例化,只有当该静态成员被**ODR-used(单一定义规则)**时,编译器才会触发其实例化和初始化。
MyFactory2的情况
MyFactory2显式定义了构造函数,并且在初始化列表中显式调用了Registrable<MyFactoryBase, MyFactory2>的构造函数。这个动作会触发对Registrable基类成员的使用——基类构造函数中对isRegistered进行了赋值操作,这属于ODR-use,因此编译器会实例化Registrable<MyFactoryBase, MyFactory2>::isRegistered,进而调用init()完成注册。
MyFactory1的情况
MyFactory1使用的是默认构造函数(MyFactory1() = default;)。对于编译器生成的默认构造函数,若基类构造函数是平凡的,且没有其他需要触发基类构造的必要操作,编译器可能不会显式生成调用基类构造的代码;同时因为没有任何代码触发Registrable<MyFactoryBase, MyFactory1>::isRegistered的ODR-use,编译器会判定该静态成员未被使用,因此不会实例化它,自然也就不会调用init()执行注册逻辑。
默认构造与显式构造的关键差异
- 默认构造函数:由编译器生成,仅在必要场景(如类的实例化、基类成员被显式引用)才会触发基类构造逻辑和相关模板成员的实例化。若无ODR-use触发,模板静态成员会被编译器优化,不执行初始化。
- 显式构造函数:哪怕是完全模仿默认行为的显式构造(如MyFactory2的构造函数),由于显式调用了基类构造函数,会强制编译器处理基类的成员使用,进而触发模板静态成员的ODR-use,执行初始化和注册逻辑。
解决方法
要让MyFactory1也能自动注册,需确保Registrable的静态成员被ODR-used,常见做法有:
- 在派生类中显式引用基类静态成员:
struct MyFactory1 : public MyFactoryBase, public Registrable<MyFactoryBase, MyFactory1> { void method() const override { std::cout << "yay Class1" << std::endl; } MyFactory1() = default; static bool dummy; }; bool MyFactory1::dummy = Registrable<MyFactoryBase, MyFactory1>::isRegistered; - 给
Registrable的构造函数添加非平凡操作,比如加入空输出语句,或用[[maybe_unused]]标记isRegistered,提示编译器不要优化该成员的使用。
内容的提问来源于stack exchange,提问作者maluyazi

