现代C++工程实践 WeakPtr 实战(三):WeakPtrFactory 与「最后成员」惯用法

现代C++工程实践 WeakPtr 实战(三):WeakPtrFactory 与「最后成员」惯用法

这个仓库已经开源!现代化 C++(C++11/14/17/20)从基础到进阶的系统教程都在这里,力争做一条完备的现代 C++ 学习路径!欢迎各位大佬前来参观,喜欢的话点个⭐!
Github 一键直达: git clone https://github.com/Awesome-Embedded-Learning-Studio/Tutorial_AwesomeModernCPP
看看超酷的新网站:https://awesome-embedded-learning-studio.github.io/Tutorial_AwesomeModernCPP/

之前那篇里咱们手搓了一枚 Flag 当铸币的原材料。但说真的,您总不能每次想发个 WeakPtr 都去 new 一个 Flag、自己盯着引用计数吧?那也太累了。Chromium 把这套琐碎活儿打包成了 WeakPtrFactory<T>------挂在被观察对象身上的一个"铸币厂":您要 WeakPtr,它铸;对象该走了,它一次性把所有铸出去的 WeakPtr 全部作废。

这一篇咱们把 factory 撸出来,顺带啃掉它最出名的那条使用规矩------WeakPtrFactory<T> weak_factory_{this} 得排成类的最后一个成员。这条惯用法看着像吹毛求疵,实际是用对用错的分界,笔者第一次见还纳闷至于这么讲究吗,后来真踩过坑才明白人家为什么特意写进头文件顶部 EXAMPLE。咱们用析构逆序把它彻底讲透。

WeakPtrFactory 内部那枚 Flag

factory 手里攥着的就一样东西:一枚 Flag,所有从它铸出去的 WeakPtr 共享同一枚。所以 factory 内部包了一个 WeakReferenceOwner------就是 Flag 的发行方兼持有方(weak_ptr.cc:82-89):

cpp 复制代码
// 发行方:持有一枚 Flag,负责失效
class WeakReferenceOwner {
public:
    WeakReferenceOwner() : flag_(make_ref<Flag>()) {}   // 构造即铸一枚新 Flag
    ~WeakReferenceOwner() {
        if (flag_) flag_->Invalidate();                 // 析构时作废所有 WeakPtr
    }
    WeakReference GetRef() const { return WeakReference(flag_); }   // 铸币
    // ...
private:
    scoped_refptr<Flag> flag_;
};

这段读起来很平实,但每一行都有戏。构造就铸一枚新 Flag;GetRef() 每次吐一个指向同一枚 Flag 的 WeakReference;析构那句 flag_->Invalidate() 才是真正埋的雷管------它让"factory 死了,所有 WeakPtr 立刻失效"成了 factory 析构函数的自动副作用,您压根不用记得去手动调 invalidate。笔者第一次读这儿的反应是:这设计真省心,把最容易忘的那一步塞进析构链里了。

WeakPtrFactory<T>WeakReferenceOwner 之上,只多加了一样东西------一个指向被观察对象的裸指针:

cpp 复制代码
// Platform: host | C++ Standard: C++20
namespace tamcpp::chrome {

template <typename T>
class WeakPtrFactory : public internal::WeakPtrFactoryBase {
public:
    WeakPtrFactory() = delete;
    explicit WeakPtrFactory(T* ptr)
        : WeakPtrFactoryBase(reinterpret_cast<uintptr_t>(ptr)) {}

    WeakPtrFactory(const WeakPtrFactory&) = delete;
    WeakPtrFactory& operator=(const WeakPtrFactory&) = delete;

    // 铸币:const factory 发 WeakPtr<const T>
    WeakPtr<const T> get_weak_ptr() const {
        return WeakPtr<const T>(weak_reference_owner_.GetRef(),
                                reinterpret_cast<const T*>(ptr_));
    }

    // 非 const 重载:发 WeakPtr<T>(requires 约束)
    WeakPtr<T> get_weak_ptr()
        requires(!std::is_const_v<T>)
    {
        return WeakPtr<T>(weak_reference_owner_.GetRef(),
                          reinterpret_cast<T*>(ptr_));
    }

    // 主动批量失效(对象还活着,但想让所有 WeakPtr 失效)
    void invalidate_weak_ptrs() {
        assert(ptr_);
        weak_reference_owner_.Invalidate();   // 失效旧 Flag + 铸一枚新 Flag
    }

    void invalidate_weak_ptrs_and_doom() {
        assert(ptr_);
        weak_reference_owner_.InvalidateAndDoom();   // 失效 + 不再铸新 Flag
        ptr_ = 0;
    }

    bool has_weak_ptrs() const { return ptr_ && weak_reference_owner_.HasRefs(); }

private:
    internal::WeakReferenceOwner weak_reference_owner_;
    // ptr_ 在非模板基类 WeakPtrFactoryBase 里,uintptr_t 类型擦除
};

}  // namespace tamcpp::chrome

代码里有几处笔者想专门点一下,用的都是之前讲过的 uintptr_t 类型擦除那套技巧。reinterpret_cast<uintptr_t>(ptr)T* 存成整数,这样能下沉到非模板基类 WeakPtrFactoryBase,省得每个 T 都重新生成一份模板代码;get_weak_ptr 里再用 reinterpret_cast<T*>(ptr_) 转回来。还有那两个 get_weak_ptr 重载,用的是成员函数 requires(!std::is_const_v<T>) 约束,const 正确性直接挂到签名上------const factory 出 WeakPtr<const T>,非 const 出 WeakPtr<T>,编译期就分得清清楚楚。

invalidate_weak_ptrs 和 invalidate_weak_ptrs_and_doom,差在哪

factory 提供了俩失效方法,光看名字您可能跟我当初一样一脸懵------这不都失效吗?差别就藏在"失效之后 factory 还能不能继续铸币"这事儿上。咱们对着 WeakReferenceOwner 的两份实现看(weak_ptr.cc:103-113):

cpp 复制代码
void WeakReferenceOwner::Invalidate() {
    assert(flag_);
    flag_->Invalidate();                 // 作废旧 Flag
    flag_ = make_ref<Flag>();            // 铸一枚新 Flag,factory 可继续铸币
}

void WeakReferenceOwner::InvalidateAndDoom() {
    assert(flag_);
    flag_->Invalidate();                 // 作废旧 Flag
    flag_.reset();                       // 不再持新 Flag,factory 进入"已死"态
}

差别就在作废旧 Flag 之后那一行。invalidate_weak_ptrs() 作废旧 Flag 后,紧接着 flag_ = make_ref<Flag>() 又铸一枚新的------所有已有 WeakPtr 集体失效,但 factory 自己还喘着气,可以接着 get_weak_ptr() 铸新的,新铸出来的 WeakPtr 之间共享那枚新 Flag。这套适合"对象进入新阶段,旧观察者该清退了,但后面还要接新观察者"的场景。

invalidate_weak_ptrs_and_doom() 就狠一点,作废旧 Flag 之后不铸新的,还顺手把 ptr_ 清零------factory 直接进入"已死"状态,之后您再调 get_weak_ptr() 拿到的就是无效结果。它比上一个更省事,连一次 Flag 分配都省了。顾名思义 doom,这是给"这对象彻底不再用了"的收尾场景准备的。

这俩的差别,后续讲性能对比时还会再露一次脸。不过说句实在话,日常九成的场景,您根本不会显式调这俩------光靠下面要讲的 factory 析构自动失效就够用了。


重头戏:「最后成员」惯用法

接下来这条是这一篇真正想讲的东西。Chromium 的 weak_ptr.h 顶部那段 EXAMPLE,专门把这条规矩写在最显眼的位置(weak_ptr.h:22-26):

Member variables should appear before the WeakPtrFactory, to ensure that any WeakPtrs to Controller are invalidated before its members variable's destructors are executed.

翻译过来一句话:成员变量得声明在 WeakPtrFactory 前面,factory 放最后。笔者第一次读到这条时心里嘀咕------至于吗,成员顺序还能影响正确性?还真能。咱们一条一条拆。

先垫个底:C++ 的析构是逆序的

这是 C++ 一条基础规则,但容易在用 WeakPtr 时被忘掉:对象析构时,成员按声明顺序的逆序 析构------先声明的后死,后声明的先死。所以如果 WeakPtrFactory 是最后一个声明的成员,它就最先 被析构;反过来您要是把它放最前面,它就成了最后才析构的那个。记住这个"逆序",下面推论全靠它。

再垫一条:factory 析构 = 失效所有 WeakPtr

前面 WeakReferenceOwner::~WeakReferenceOwner() 里那句 flag_->Invalidate() 是关键------factory 一析构,它铸出去的所有 WeakPtr 跟着作废。换句话说,"factory 什么时候死"直接决定了"所有 WeakPtr 什么时候失效"。

两条叠起来:为什么 factory 非得放最后

把上面这两条拼一块儿,推论自己就冒出来了。咱们假设 Controller 有几个普通成员外加一个 factory,然后故意把两种声明顺序都写一遍对比:

cpp 复制代码
// ✗ 错误顺序:factory 放前面
class BadController {
public:
    void on_work_done() { /* 用 buf_ */ }
private:
    WeakPtrFactory<BadController> weak_factory_{this};   // 先声明 → 最后析构
    std::vector<int> buf_;                                // 后声明 → 先析构
};

// ✓ 正确顺序:factory 放最后
class GoodController {
public:
    void on_work_done() { /* 用 buf_ */ }
private:
    std::vector<int> buf_;                                // 先声明 → 最后析构
    WeakPtrFactory<GoodController> weak_factory_{this};   // 后声明 → 先析构
};

先看 BadController 这版反例。析构按逆序走:buf_ 先死,然后才轮到 weak_factory_,这时候才想起来失效所有 WeakPtr。问题就出在这俩中间那个窗口------buf_ 已经析构了,可所有 WeakPtr 还活蹦乱跳地"有效"着。偏偏这种时候要是有个异步任务拿着 WeakPtr 解引用 Controller,它顺顺当当进 on_work_done(),一头撞上已经析构的 buf_。UAF,稳稳的。笔者当年在类似场景里查过一个偶现的 ASAN 报红,根因就是这个顺序。

GoodController 就把顺序倒过来了:weak_factory_ 最后声明,所以最先析构,一析构就把所有 WeakPtr 作废,然后 buf_ 才轮到死。等到 buf_ 真的开始析构时,外头已经没有任何"有效"的 WeakPtr 能碰到它了,后续解引用顶多拿到个 nullptr。安全。

「最后成员」惯用法,根子就这么一句话:让 factory 在其它成员之前先析构,借它析构时那一下自动失效,把其它成员的析构期给罩住。

一个容易踩的边界:守护的是成员析构期,不是析构函数体

这里有个细节笔者特意要讲清楚,因为它最容易让人误判------咱们研究核验的时候专门确认过这一条。factory 放最后,罩住的是成员的析构期 ;它拦着您在对象自己的析构函数体里用 WeakPtr。咱们把时间线摊开看:

text 复制代码
GoodController 析构:
  ① 析构函数体执行(此时所有成员还活着,WeakPtr 仍有效)
  ② 成员按逆序析构:
       weak_factory_ 先析构 → 所有 WeakPtr 失效  ← 守门发生在这里
       buf_        后析构

也就是说,在 ① 析构函数体执行那会儿,WeakPtr 还有效。这其实是符合直觉的------析构体里您常常还想引用同对象的其它成员(比如通知观察者一句"我要走了"),这种时候 WeakPtr 有效反而顺手。真正作废发生在 ② 进入成员析构之后,目的就是确保后续再有任何 deref 都碰不到半销毁的成员。这条边界 Chromium 源码注释里没明写,笔者是读着代码自己推出来的,您记一下,免得日后以为"对象一进析构 WeakPtr 就立刻失效"而误判。


为什么 Chromium 只留了组合式这一条路

您翻遍 Chromium 现在的 //base,能拿到 WeakPtr 的官方姿势就一种:把 WeakPtrFactory<Controller> weak_factory_{this} 当成员塞进 Controller。这就是「最后成员」惯用法的载体,Chromium 选它是有理由的------失效时机您说了算,能对非自身类型用(极端点,WeakPtrFactory<bool> 都行),也不污染继承链。

其实历史上还有过一种继承式的 SupportsWeakPtr<T>,继承它就白得一个 GetWeakPtr()。听上去更省事对吧?但它把人往不安全的用法上带,后来 Chromium 索性从 //base 里挪走了,现在 weak_ptr.h 里 grep SupportsWeakPtr 已经搜不到。笔者提它一嘴不为别的,就怕您翻老代码、老文档时撞见这名字发懵。新代码一律走组合式,理解了组合式,那套老机制无非是把 factory 藏进基类,本质没差。


把之前那个坑用 factory 重写一遍

光说不练假把式,咱们把之前那个悬空回调的场景,用刚撸好的 factory 重写一遍,看它怎么把 UAF 给堵死的:

cpp 复制代码
// Platform: host | C++ Standard: C++20
#include <functional>
#include <iostream>
#include <vector>

class Controller {
public:
    void on_work_done(int v) {
        buf_.push_back(v);
        std::cout << "got " << v << ", buf size=" << buf_.size() << '\n';
    }
    WeakPtr<Controller> get_weak() { return weak_factory_.get_weak_ptr(); }

    ~Controller() = default;
private:
    std::vector<int> buf_;                                  // 先声明
    WeakPtrFactory<Controller> weak_factory_{this};         // 最后成员!
};

int main() {
    using namespace tamcpp::chrome;
    WeakPtr<Controller> wp;                                 // 先声明,待会儿填

    {
        Controller c;
        wp = c.get_weak();
        std::cout << (wp ? "alive" : "dead") << '\n';       // alive
        if (wp) wp->on_work_done(7);                        // got 7, buf size=1
    }   // c 离开作用域:weak_factory_ 先析构 → wp 失效 → buf_ 才析构

    std::cout << (wp ? "alive" : "dead") << '\n';           // dead
    if (wp) {
        wp->on_work_done(8);                                // 不会进这里
    } else {
        std::cout << "controller gone, skip\n";             // 走这条
    }
    return 0;
}

咱们跑一下看看。输出会按顺序蹦出来:alivegot 7, buf size=1deadcontroller gone, skip。重点盯最后一行------Controller 析构之后,wp 已经自动失效,if (wp) 这道闸把对已析构对象的访问拦在了外面。之前那个让人头疼的悬空回调,到这儿被 factory 析构时的自动失效给彻底堵死了,您一行防护代码都不用写。

到这里 WeakPtr 的几样核心机制------铸币、失效、解引用守门------算是凑齐了。不过咱们还有一条使用契约一直绕着没讲:WeakPtr 的解引用和失效,得发生在绑定时的同一个序列上。这条契约值得正面掰扯清楚,顺带把 factory 的 lazy 序列绑定也讲一下,下一篇见。

参考资源

相关推荐
IT小盘4 小时前
04-大模型流式输出原理-SSE与Python实现
开发语言·网络·人工智能·python
不如语冰4 小时前
AI大模型入门-Python进阶-上下文管理与with语句
开发语言·数据结构·数据库·人工智能·pytorch·redis·python
JetComXCpp4 小时前
给 Win32 窗口加上毛玻璃效果——纯 D3D11 实现,零依赖
c++
阿童木写作5 小时前
Python批量翻译亚马逊商品图实战教程
开发语言·python·xcode
会飞的大鱼人6 小时前
一文搞懂 Java HashSet:把它想成游乐园里只允许一次入场的盖章名单
java·开发语言·windows
ysa0510306 小时前
【板子】二分答案(最大最小?)
c++·笔记·算法·板子
jiay26 小时前
【.net10】顶级程序语句
java·开发语言
stolentime7 小时前
SP8549 MAIN75 - BST again题解
c++·算法·二叉树·深度优先·图论·记忆化搜索·组合数学
淼澄研学7 小时前
Python进阶实战:深入解析推导式与生成器等5大核心特性
开发语言·python