一篇9月4日发布的技术博客,把Rust的dyn Trait内存布局拆得明明白白。作者是个C++背景的Rust新手,用transmute把一个胖指针硬生生掰成两个数字,肉眼可见"数据指针+vtable指针"的结构,还顺手发现Rust里一个空结构体的size_of居然是0——这在C++里是违规的,标准规定每个对象至少占1字节。文章写得清楚,读着也爽。

问题是,那种"这就是Rust真相"的确定语气,恰恰是Rust官方文档反复叫人别用的语气。文中拆出来的很多"事实",官方自己标注的身份是"当前实现,不作保证"

胖指针里到底装了什么

C++的虚函数表指针长在对象身体里,对象一出生就带着它。Rust的dyn Trait不这样干:一个&dyn Draw是两个指针拼起来的胖指针,一个指向数据,一个指向vtable,类型信息是调用时才现场配对的。这也是为什么同一个Duck同时实现FlySwim,两个trait对象共享同一个数据指针,却各自挂着不同的vtable。

&dyn Draw 胖指针结构 数据指针 指向 Circle 实例 vtable 指针 指向静态 vtable vtable 内容(概念性布局) size · align · drop_in_place · draw() 官方: 字段顺序不保证

这个结构本身没问题,原文讲得也直观。真正被跳过的一步,是官方文档就在同一份Reference里,紧跟着这套模型写了一句话:胖指针恰好是两个usize大小,只是当前实现,不是语言承诺。DynMetadata文档说得更直白——vtable的字段排布是"概念性"的,不构成稳定ABI。

官方标了警戒线,笔记却没抄

这不是吹毛求疵。Rust核心库源码里还有一条更扎眼的警告:vtable指针的相等性不能拿来判断具体类型身份。同一个实现的vtable可能因为codegen unit被复制出好几份,内容相同的不同vtable也可能被编译器去重合并成一份。也就是说,你今天写代码比较两个vtable指针是否相等,来判断"这是不是同一个类型",这个判断在不同编译版本、不同优化级别下都可能翻车。

笔记的确定语气 vs 官方的边界标注 笔记里的说法 胖指针 = 两个usize 同类型共享一个vtable ZST身份靠所有权追踪 CRTP≈单态化 官方标注的边界 "仅是当前实现" vtable可被复制/去重 只谈地址,不谈所有权 二者是不同层面的东西

原文另一个被指出的问题,是把C++的CRTP直接等同于Rust的单态化。普通C++模板本身就按具体类型实例化,CRTP只是利用模板玩出的一种静态多态写法,两者不是一回事。作者把ZST身份问题归因于"Rust靠所有权而非地址追踪身份",也把两个不相干的概念焊在了一起——所有权管的是谁能用这个值,地址身份管的是这个值住在哪儿,更严谨的说法应该是"零尺寸值的引用不保证有不同地址",跟所有权没有必然关系。

至于文中反复用来做实验的transmute,把胖指针硬拆成(usize, usize)观察——这本身需要unsafe,恰恰说明编译器自己都不担保这份布局对下一个版本还成立。这是实现观察,不是语言语义。文章配套的代码仓库目前访问已经404,搜索也没找到任何人对这些实验做过公开审阅或勘误,这些"意外发现"目前只是一个人的探索记录,没有经过社区校验。

  • 风险.把rustc当前行为当成语言规则内化,编译器换版本、换优化级别,基于错误心智模型写的代码可能悄悄变脆弱。

dyn兼容(原称object safety)的规则也比原文暗示的更细。原文只提了"方法不能返回Self""方法不能有泛型参数"两条,但官方规则里还留了where Self: Sized这类例外——加上这个约束的方法,即便签名看起来违规,也可以被排除在vtable之外,让整个trait依然可以做成trait object。这类细节教程里经常被简化掉,却恰恰是新手踩坑最多的地方。

眼见为实,未必为真。

古人讲"名不正则言不顺",技术文档里也是一样:一份把"实现细节"和"语言保证"混着讲的教程,读起来流畅,用起来危险。这篇笔记的价值不小——它把trait object在内存里的样子讲得比大多数入门材料都直观,ZST那一段的"WHAT???"式惊讶也是真实的学习反应,值得肯定。但它缺了一道免责声明:哪些是Rust承诺给你的,哪些只是rustc今天心情好。

  • 结论.遇到用transmuteunsafe、"我做了个实验发现"这类表述的教程,先把它当成观察记录,别当成语言规范去背。真正想确认布局稳不稳,去翻Reference和源码里那句"不保证",比信任任何一篇个人博客都靠得住。