多级CRTP初始化异常:构造阶段成员指针反复为空问题排查
基于CRTP多级继承的构造阶段空指针问题分析
问题描述
我尝试实现基于CRTP的多级继承,期望构造函数参数在构造过程中修改中间层成员。最初遇到base构造函数调用print时,implVal尚未分配导致运行时错误;将implVal改为std::shared_ptr并按需分配后,base的print可正常运行,但implA构造函数执行时implVal又变为空指针,引发空指针错误,请问这是什么原因?
代码示例
#include <iostream> #include <string> #include <memory> struct Val { std::string str; void append(const std::string n) { str += n; } Val() : str("x") { } }; template<typename T> class base { public: base(int n) { std::cout << "Base " << n << "\n"; // triggers error if not ptr print(); } void print() { static_cast<T*>(this)->impl_print(); } ~base() { std::cout << "DB\n"; } }; template<typename T, typename V> class implA : public base<implA<T, V>> { friend class base<implA<T, V>>; public: implA(int n) : base<implA<T, V>>(n) { std::cout << "implA " << n << "\n"; for (int i = 0; i < n; i++) { implVal->append(std::to_string(n)); } } ~implA() { std::cout << "iA\n"; } protected: std::shared_ptr<V> implVal = NULL; void impl_print() { if (implVal == NULL) { implVal = std::make_shared<V>(); } static_cast<T*>(this)->impl_print(*implVal); } }; class classX : public implA<classX, Val> { friend class implA<classX, Val>; public: classX(int n) : implA<classX, Val>(n) { std::cout << "classX " << n << "\n"; } protected: void impl_print(Val in) { std::cout<<"p"<< in.str << "\n"; } }; int main() { classX x(2); x.print(); return 0; }
问题原因
1. 成员变量初始化顺序覆盖临时赋值
C++类的构造执行顺序是固定的:
- 先执行所有基类的构造函数(从最顶层基类开始)
- 然后初始化当前类的所有成员变量
- 最后执行当前类的构造函数体
在你的代码中:
base构造函数调用print(),触发implA::impl_print(),此时implA的implVal还未执行成员初始化(基类构造优先于成员初始化),所以你在impl_print()里将implVal赋值为有效指针。- 当
base构造完成后,进入implA的成员初始化阶段,implVal会被重新赋值为NULL(你在成员声明处的默认初始化),直接覆盖了之前在impl_print()中的赋值。 - 等到
implA构造函数体执行时,implVal已经变回空指针,调用implVal->append()自然触发空指针错误。
2. CRTP构造阶段的未定义行为
在CRTP中,基类构造函数调用派生类(或中间层)的成员函数是危险的——此时派生类/中间层的对象尚未完全初始化,成员变量可能处于未定义状态,这种操作属于C++标准中的未定义行为,即使临时解决了指针问题,后续也可能出现其他不可预料的错误。
解决方案
方案1:调整成员初始化逻辑
移除implVal的默认初始化,改为在implA的构造初始化列表中显式初始化,避免覆盖基类构造阶段的临时赋值:
template<typename T, typename V> class implA : public base<implA<T, V>> { friend class base<implA<T, V>>; public: // 在初始化列表中显式初始化implVal implA(int n) : base<implA<T, V>>(n), implVal(std::make_shared<V>()) { std::cout << "implA " << n << "\n"; for (int i = 0; i < n; i++) { implVal->append(std::to_string(n)); } } ~implA() { std::cout << "iA\n"; } protected: // 移除默认初始化 std::shared_ptr<V> implVal; void impl_print() { static_cast<T*>(this)->impl_print(*implVal); } };
方案2:禁止在基类构造阶段调用依赖成员的函数
修改base的构造函数,移除对print()的调用,避免在对象未完全初始化时触发成员函数调用:
template<typename T> class base { public: base(int n) { std::cout << "Base " << n << "\n"; // 移除构造阶段的print()调用 // print(); } void print() { static_cast<T*>(this)->impl_print(); } ~base() { std::cout << "DB\n"; } };
经过以上修改后,构造过程会按照正常顺序执行,implVal始终保持有效指针,不会出现空指针错误。
内容的提问来源于stack exchange,提问作者Treeman
相关产品推荐
相关产品推荐

