开发者生态
morning
可视化 Rust 的 Vtable:dyn Trait 如何在内存中工作
2026-09-06
1 阅读
约6分钟阅读
torutofu
字号:
我正在尝试 Rust,它既令人满意又令人难以置信。到目前为止,我一直在从这本书和马拉博斯的书中学习,但我渴望对自己进行一些解剖。我这些实验的最初目标是将 Rust 的多态性方法与 C++ 的方法进行比较。然而,最终,正如我逐渐意识到的那样,当试图通过另一种语言来理解一种新语言时,试图进行一一对应,这有点陷阱。这看起来似乎有帮助,但归根结底,我们不能将 Rust 视为具有不同语法的 C++。如果真是这样,那就没什么革命性的了。也就是说,我相信探索并理解其中的原因是有好处的。所以,如果你像我一样,需要知道内存中究竟发生了什么,以便让你真正理解这些概念,希望你会发现这篇文章很有用:)顺便说一句,缩略图是一张锈菌的照片,我们把 Rust 的名字归功于它。图片来源:来自英国马尔文 Cradley 的 gailhampshire,CC BY 2.0,来自 Wikimedia Commons。您可以在 GitHub 上找到所有代码和实验。简介:问题的关键我们想要实现的目标非常简单。假设我们有一堆形状:圆形、正方形、三角形,并且我们想对每个形状调用draw()。 C++ 方法#1:虚函数 在 C++ 中,第一个想到的方法是通过虚函数,它利用运行时多态性。 vtable 指针位于对象内部,虚拟调度会自动发生。 std::vectorshapes = { new Circle(), new Square() }; for ( auto * s : 形状 ) s -> 绘制 (); Rust 的等价物是 dyn Trait ,这就是我们最终想要理解的。但首先,让我们看一下在 C++ 中解决这个问题的另一种方法。 C++ 方法#2:CRTP 人们还可以采用 CRTP(奇怪的重复模板模式)路线,这本质上是编译时多态性。如果您感兴趣,Klaus Iglberger 的精彩演讲是我第一次介绍该主题,也是我不断回顾的演讲。 template < typename Derived > struct Shape { void draw () { static_cast < Derived *> ( this ) -> draw (); } } };本质上,没有虚函数表,它在编译时解析,牺牲了可读性(确实很拗口)。 Rust 提供了一个更直接、更简单的 CRTP 等价物,即单态化。这是我们首先要深入研究的方法,以开始构建 Rust 所提供的功能的心理模型。静态调度 照片由 Jiawei Zhao 在 Unsplash 上拍摄 静态调度(也称为泛型)实现了与 CRTP 类似的结果:编译器为调用它的每种类型生成该函数的单独副本。运行时成本为零,但类型必须在编译时已知。特征 Draw { fn draw ( & self ) -> & str ; } 结构体圆;结构体正方形; impl Draw for Circle { fn draw ( & self ) -> & str { "画圆" } } impl Draw for Square { fn draw ( & self ) -> & str { "画正方形" } } fn draw_shape < T : Draw > ( shape : T ) { println! (“{}”,形状.draw()); } fn main () { 让圆 = 圆;让平方=平方;绘制形状(圆);绘制形状(正方形);在底层,编译器生成两个独立的函数:draw_shape::和draw_shape::。这与 C++ 的模板相比如何?这里的区别在于哲学。 C++ 使约束变得隐式,模板接受任何恰好具有 .draw() 方法的类型 T。而在 Rust 中,您明确地输入了合同:您“为 Square 实现 Draw 特征”。我的问题是,什么时候这还不够?在解决这个问题之前,让我们先做一个支线任务。支线任务:Rust 的零大小类型 我尝试查看 Circle 和 Square 的大小,因为我想与宽指针进行比较,我们稍后会看到这一点,但这让我发现了一些意想不到的东西。在 C++ 中,标准要求每个对象的大小至少为 1 个字节,即使是空的也是如此。这样两个不同的对象始终具有不同的地址,这意味着 &obj1 必须与 &obj2 不同。这是我心中根深蒂固的自然事实之一,所以看到 rust 返回 0 让我感到非常惊讶。当我探索 Rust 时,这些小时刻给我带来了很多快乐,因为它解构了我的心理模型并帮助我欣赏不同的哲学。打印! ( " {} " , std::mem::size_of::()); // 0 什么???打印! (“{}”,std::mem::size_of::()); // 0 这就是我发现 Rust 以不同方式处理唯一地址保证的方式。零大小类型(ZST)是不包含任何字段的结构,因此不需要分配任何内存。 Rust 通过所有权而不是地址来跟踪身份。每个值一次只有一个所有者,这是由我们的朋友借用检查器在编译时强制执行的。在 C++ 中,我们可能会这样做
这篇文章对您有帮助吗?
订阅66必读
每日精选科技资讯,直达你的邮箱