You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

多级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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.20 20:15:15