C++编程C++ 开发者

内部弱指针的具体初始化状态是什么,导致在从 `std::enable_shared_from_this` 继承的类的构造函数中调用 `std::shared_from_this()` 时抛出 `std::bad_weak_ptr`?

用 Hintsage AI 助手通过面试

问题的答案

std::enable_shared_from_this 是一个混合基类,它封装了一个私有的可变 std::weak_ptr<T> 成员,通常命名为 weak_this。在派生对象的构造过程中,这个内部的 weak_ptr 经过默认构造,处于一个空(过期)状态。关键的架构细节是,这个内部指针被初始化以引用控制块仅发生在 std::shared_ptr 构造函数中, 被管理对象的构造函数完成之后。因此,在构造函数体内调用 shared_from_this() 会尝试在一个空的 weak_ptr 上调用 lock(),根据 C++17 的规定,会抛出 std::bad_weak_ptr 异常(或在早期标准中导致未定义行为),因为提供新引用所需的共享所有权基础设施尚未建立。

生活中的情况

背景:

一个高频交易平台实现了一个 MarketDataHandler 类,用于管理与股票交易所的持久 TCP 连接。为了确保处理程序在异步套接字读/写操作期间保持活跃,该类继承自 std::enable_shared_from_this<MarketDataHandler>。构造函数接受连接参数并立即启动异步读取操作,将 shared_from_this() 作为完成处理程序传递给 Boost.Asio 事件循环。

问题:

在集成测试期间,应用程序在连接建立时立即崩溃,未捕获的 std::bad_weak_ptr 异常终止了进程。开发团队假设,由于基类 std::enable_shared_from_this 子对象在派生类构造函数体执行之前被构造,内部跟踪机制应该可以立即使用。他们未考虑对象构造和 std::shared_ptr 包装器完成之间的时间间隔,这使得内部 weak_ptr 在工厂表达式完成之前处于未初始化状态。

考虑过的替代解决方案:

通过 post_construct() 的两阶段初始化:

重新构造类,将所有异步初始化逻辑从构造函数移动到一个独立的 post_construct() 公共方法中。调用者首先使用 std::make_shared<MarketDataHandler> 创建一个 std::shared_ptr<MarketDataHandler>,然后在返回指针给系统之前立即调用 post_construct()

  • 优点: 实现简单,对现有类层次结构的结构性改变最小。
  • 缺点: 通过引入外部初始化要求违反 RAII 原则;创建了一个 "僵尸" 状态,即对象存在但未完全功能;调用者可能忘记调用 post_construct(),导致处理程序永远无法开始处理数据的微妙错误。

具有外部生命周期保证的原始指针:

将原始 this 指针传递给异步 I/O 系统,并使用 std::shared_ptr 键维护一个活跃连接的全局注册表,在每次回调执行时检查注册表成员资格。

  • 优点: 在构造期间允许立即注册,无需 shared_from_this()
  • 缺点: 手动生命周期管理违背了智能指针的目的;为全局注册表引入复杂的同步要求;如果回调在快速连接更换期间超出注册表清理逻辑的寿命,极易发生使用后释放错误。

带有私有构造函数的静态工厂方法:

使所有构造函数私有,并提供一个公共静态 create() 方法,返回一个 std::shared_ptr<MarketDataHandler>。在 create() 内部,该方法首先使用 std::make_shared 构造对象,然后使用结果共享指针启动异步操作,然后将其返回给调用者。

  • 优点: 强制保证没有 MarketDataHandler 存在而没有被 std::shared_ptr 拥有;保证初始化的原子性;防止出于共享所有权而严格意图的对象堆栈分配的危险。
  • 缺点: 除非工厂被声明为朋友,否则阻止使用带有私有构造函数的 std::make_shared;需要稍微冗长的语法(MarketDataHandler::create() 对比 std::make_shared<MarketDataHandler>())。

选择的解决方案:

选择了静态工厂模式,因为它消除了在未拥有对象上调用 shared_from_this() 的可能性。通过将构造限制在 create() 方法中,我们确保了 std::shared_ptr 控制块始终完全构造并初始化了内部 weak_ptr,在任何方法尝试发出附加引用之前。

结果:

重构消除了所有启动崩溃。代码库采用了一种稳健的异步对象创建模式,并在网络层中应用一致。代码审查指南更新,禁止在工厂构造后调用的任何方法外调用 shared_from_this(),显著降低了与生命周期相关的缺陷率。

候选人常常忽视的内容

问题: shared_from_this() 是否增加引用计数,它如何与控制块交互?

回答:

shared_from_this() 不会创建新的控制块。相反,它访问存储在 std::enable_shared_from_this 基类中的内部可变 std::weak_ptr<T> 成员,并在其上调用 lock()。该操作原子性地检查控制块是否仍然存在,如果存在,则增加与现有控制块关联的强引用计数,返回一个新的 std::shared_ptr 实例,共享所有权。如果对象已经被销毁(过期的弱指针),lock() 返回一个空的 std::shared_ptr。候选人常常错误地认为 shared_from_this() 只是返回某个内部 shared_ptr 的副本,忽略了它实际上是将弱引用提升为强引用,这对于避免 "双重所有权" 场景至关重要,在这种情况下,两个独立的 std::shared_ptr 实例可能会分别跟踪同一个对象,具有单独的引用计数。

问题: 一个类可以多次从 std::enable_shared_from_this<T> 继承,或者通过菱形继承中的多条路径继承吗?

回答:

一个类不能直接从同一个 Tstd::enable_shared_from_this<T> 多次继承,因为这会导致模糊的基类子对象。然而,一个类 Derived 应当仅从 std::enable_shared_from_this<Derived> 继承,而不是从基类的版本继承。候选人遗漏的关键细节涉及虚拟继承:如果 Basestd::enable_shared_from_this<Base> 继承,并且 DerivedBase 继承,那么从 Derived 内部调用 shared_from_this() 会在 Base 指针上正常工作,因为内部 weak_ptr 被初始化以指向最派生的对象。然而,如果 Derived 也公开继承自 std::enable_shared_from_this<Derived>,这会导致两个独立的 weak_ptr 成员,造成初始化的哪个成员产生混淆。标准规定,std::shared_ptr 构造函数的初始化专门查找 std::enable_shared_from_this 特化;拥有多个独立的 weak_ptr 成员会导致只有一个被初始化(通常与用于创建第一个 std::shared_ptr 的静态类型相关的那个),可能使其他成员为空,并导致后续的 shared_from_this() 调用失败。

问题: std::make_sharedstd::shared_ptr<T>(new T) 在构造期间对于 shared_from_this() 的安全性是否不相关?

回答:

两种分配策略最终都会调用 std::shared_ptr 构造函数,该构造函数通过模板元编程检测 std::enable_shared_from_this 基类。内部 weak_ptr 的初始化严格发生在 std::shared_ptr 构造函数逻辑中,而不是在执行 new Tmake_shared 的内部对象构造阶段期间。具体来说,make_shared 分配存储,构造 T 对象(在此期间,weak_ptr 仍然为空),然后才是 std::shared_ptr 构造函数初始化 weak_ptr 以指向新创建的控制块。候选人常常假设 make_shared 可能由于其单次分配优化而以某种方式较早 "准备" 对象,但标准保证从构造函数体调用 shared_from_this() 是不安全的,无论使用了哪个工厂函数,因为 weak_ptr 分配严格发生在 T 构造函数完成之后。