C++中父对象唯一持有子对象时如何为子对象传递指向自身的weak_ptr
1. 与直接使用普通引用相比,持有父对象的weak_ptr的核心优势
普通引用的本质是无生命周期管控的语法糖,作为类成员长期存储时,它的两个天生缺陷刚好被weak_ptr解决:
- 避免悬空访问的兜底能力:普通引用一旦绑定就没有办法感知绑定对象的销毁,一旦父对象先于子对象析构,子对象持有的引用就会变成悬空引用,调用时直接触发未定义行为。而
weak_ptr与对象的生命周期深度绑定,每次使用前可以通过lock()尝试获取有效的shared_ptr,如果返回空就说明父对象已经被销毁,完全可以提前做空值判断、错误处理,从机制上杜绝悬空访问的可能。 - 更高的场景适配灵活性:普通引用一经绑定就无法修改,如果后续业务需要把 Child 从一个父对象迁移到另一个父对象下,普通引用根本无法重新绑定,必须重构对象的构造逻辑。而
weak_ptr和普通指针一样可以随时重新赋值,适配动态调整归属关系的场景成本极低。
注:如果能100%确定父对象的生命周期一定长于子对象、且归属关系永远不会变动,普通引用的开销更低是完全可以用的。但现实项目里的代码迭代,没人能永远保持这个100%的确定性:哪怕在你写的结构里 Child 是 Parent 的直接成员(按析构顺序本来就不会悬空),只要哪天有人在 Child 的逻辑里把 parent 指针传到异步线程、全局回调队列里,等 Parent 已经析构完了任务才执行,之前靠口头约定写的引用直接就会触发崩溃,weak_ptr的零成本校验相当于给这类意外场景加了层自动防护。
2. 代码中传递父对象weak_ptr的实现方法
首先直接解答关于enable_shared_from_this的疑问:根本不需要单独的weak_ptr版本的enable_shared_from_this,标准库的std::enable_shared_from_this已经内置了直接生成weak_ptr的能力——只要让Parent类公有继承std::enable_shared_from_this<Parent>,直接调用实例的weak_from_this()方法,就能拿到和当前对象生命周期绑定的合法weak_ptr<Parent>,没有任何额外的样板代码。
具体改造步骤
- 给
Parent类加上enable_shared_from_this的继承,注意这个机制的生效前提是:当前的Parent对象必须是被std::shared_ptr全局管理的,如果是栈对象、或者仅用unique_ptr管理的对象,调用weak_from_this()会直接抛出std::bad_weak_ptr异常,这是新手最容易踩的坑。 - 在
add_child方法里,直接调用weak_from_this()拿到父对象的weak指针传给Child即可:
#include <memory> #include <vector> class Parent; class Child { public: void set_parent(std::weak_ptr<Parent> parent) { parent_ = parent; } std::weak_ptr<Parent> parent_; }; // 加上enable_shared_from_this的公有继承 class Parent : public std::enable_shared_from_this<Parent> { void add_child(std::unique_ptr<Child> child) { // 直接调用内置的weak_from_this()即可拿到合法的weak_ptr child->set_parent(weak_from_this()); children_.push_back(std::move(child)); } std::vector<std::unique_ptr<Child>> children_; };
关键避坑提醒
千万不要图省事直接写child->set_parent(std::weak_ptr<Parent>(this))这种代码——这种写法会给this指针单独创建一个和原控制块完全割裂的独立weak_ptr,完全没有办法感知原对象的销毁,等原Parent对象被shared_ptr回收后,这个weak_ptr照样是悬空的,和裸指针没有任何区别。
非shared_ptr管理场景的替代方案
如果你的Parent对象完全没有被shared_ptr管理(比如就是栈上的局部对象,不想引入shared_ptr的额外控制块开销),那上面的weak_from_this方案就用不了,这时候只要严格保证Parent对象的生命周期长于所有Children(也就是给定代码里的结构——Parent持有Children的独占所有权,Children析构一定早于Parent),直接用裸指针存parent引用即可,开销更低,也不会出现悬空问题。
内容的提问来源于stack exchange,提问作者user6429576

