std::bad_weak_ptr 这玩意儿,但凡用 shared_ptr 写过点东西的人,大概率都撞到过。报错瞬间往往是一脸懵:明明代码编译过了,逻辑看着也没问题,运行时就给你抛个异常,而且还是个冷门异常。更气人的是,它不像 std::bad_alloc 那样查查文档就能明白,很多情况下是“知其然不知其所以然”,网上资料也少,翻半天找不着一个能把来龙去脉讲清楚的。
这篇文章就专门来拆解 std::bad_weak_ptr。我会从它的定义和抛出条件讲起,分析几个最常见的触发场景,再结合排查经验和防御性编程手段,把这玩意儿的底裤扒干净。无论你是刚接触智能指针的新手,还是被线上问题折磨过的老手,这篇文章都值得看完。
1. 异常的本质:std::bad_weak_ptr 到底在什么情况下抛出
先给不熟悉的读者打个底。std::bad_weak_ptr 是 C++ 标准库定义的一个异常类,继承自 std::exception,定义在 <memory> 头文件里。它的 what() 返回值通常是 "std::bad_weak_ptr" 或者 "bad_weak_ptr",具体字符串取决于标准库实现。
它的抛出条件其实非常明确:从一个 std::weak_ptr 构造 std::shared_ptr 的时候,如果这个 weak_ptr 已经失效(expired),也就是它所指向的对象已经被销毁,那么就会抛出 std::bad_weak_ptr。
听起来很简单对吧?但问题就出在很多程序员以为“我用 weak_ptr.lock() 来判断失效不就行了”,结果却栽在 shared_ptr 的另一个构造路径上。
1.1 构造函数中的隐式陷阱:shared_ptr(const weak_ptr&) 与 shared_ptr(weak_ptr&&)
std::shared_ptr 有两个构造函数接受 weak_ptr 参数:
cpp复制template<class Y>
explicit shared_ptr(const std::weak_ptr<Y>& r);
template<class Y>
explicit shared_ptr(std::weak_ptr<Y>&& r);
这两个构造函数的行为是:如果 r.expired() 为真,就抛出 std::bad_weak_ptr;否则,接管 r 所引用的对象的所有权。
注意,它们是 explicit 的,所以不能隐式转换,你不可能在代码里写 shared_ptr<int> sp = wp; 然后期望它默默转换——编译直接报错。但如果你写 shared_ptr<int> sp(wp);,那就是明确调用了这个构造函数,此时如果 wp 已失效,异常就来了。
这里有个关键点:weak_ptr 本身不持有对象的所有权,它只是观测者。 当它观测的对象被最后一个 shared_ptr 释放后,weak_ptr 就自动变为 expired 状态。此时你无论调用 lock() 还是用这个构造函数,都无法再获得有效的 shared_ptr。
区别在于:
lock()是安全的选择——如果 expired,返回一个空的shared_ptr,不抛异常。- 构造函数则直接抛异常。
很多老代码或者从别处抄来的代码,可能没注意到这个区别。我自己遇到过几次,都是因为重构时把 lock() 换成了构造函数,然后线上就开始疯狂抛异常。
1.2 异常抛出的底层机制:控制块与引用计数
要真正理解这个异常,还得从 weak_ptr 的底层机制说起。标准库实现中,shared_ptr 和 weak_ptr 共享一个控制块(control block),控制块里维护两个计数:强引用计数(use_count)和弱引用计数(weak_count)。
当强引用计数降为 0 时,被管理的对象就被销毁,但控制块本身还会保留,直到弱引用计数也降为 0。此时 weak_ptr 的 expired() 就会返回 true,因为它内部保存的指针已经指向一个被销毁的对象。
用伪代码来理解:
text复制控制块 {
T* ptr; // 被管理对象指针
size_t strong_count; // shared_ptr 数量
size_t weak_count; // weak_ptr 数量
};
当 strong_count 从 1 减到
