Can someone explain or point me to the article that explains how vtables implementation in rust differs from C?
Thank you
Can someone explain or point me to the article that explains how vtables implementation in rust differs from C?
Thank you
Rust and C++ both implement runtime polymorphism using tables of function pointers, but they differ in both what the vtable contains and where it exists.
In C++, the vtable contains pointers to virtual methods and possibly other information like RTTI (Run-Time Type Information) or virtual base class offsets. Whereas in Rust, the vtable contains pointers to the methods defined in the trait, its layout (i.e. size and alignment) and a pointer to the destructor of the type implementing the trait. It does not contain any other type-specific information, in particular, it doesn't include the value's original type so there is no builtin equivalent to std::dynamic_cast
.
C++ is kinda hamstrung when it comes to where that vtable is placed because it needs Foo *
to be a single pointer. That means the object itself contains the vtable pointer (usually the first member), followed by the object's data members. When inheritance is involved, the object layout and vtables can become a lot more complex.
In single inheritance, the vtable of a derived class includes the entries for the virtual methods of the base class, followed by entries for any new virtual methods in the derived class. When a derived class overrides a virtual method, the corresponding entry in the vtable is updated to point to the derived class's implementation.
In multiple inheritance, each base class has its own vtable. The derived class has a separate vtable for each base class it inherits from, and each vtable follows the same rules as in single inheritance. The object layout for the derived class will include a vtable pointer for each base class, in the order they are declared in the inheritance list.
In Rust, a &dyn Trait
isn't a single pointer, but actually a pointer with some extra metadata. You have a pointer to the data (the actual value implementing the trait) and another pointer to the vtable for the trait. The data and the vtable are separate, and the layout of the data is not affected by the presence of the vtable. It doesn't even know that it might be used with virtual dispatch.
For example, given the following trait:
trait Debug {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result;
}
A &dyn Debug
would be represented like this:
struct &dyn Debug {
data: *mut (),
vtable: &'static DebugVTable,
}
struct DebugVTable {
drop_in_place: fn(*mut ()),
size: usize,
alignment: usize,
fmt: fn(*mut (), &mut Formatter<'_>) -> fmt::Result,
}
Note that the way things are laid out in Rust are implementation details of the compiler and subject to change (e.g. future RFCs might change the layout of &dyn Debug
to allow a trait object to have multiple vtables or enable downcasting to a super-trait).
Does it not also contain the size and alignment of the type?
Thanks. I mentioned it in the example DebugVTable
struct but forgot to mention the layout in my explanation. I've updated the original post.
This^^^. Thanks. I was reading about it somewhere and couldn't find that.
Is my understanding correct that the layout of Rust vtable is similar to this pic:
Trait -> Struct that implements that trait
-> vtable
?
The most obvious difference is that the C language does not use V-tables, it has no need for them.
Hehe, you are correct. Luckily the other people understood what I meant.
I would draw it like this:
&dyn Foo (Trait Object)
+--------+----------+
| Data * | Vtable * |
+--------+----------+
| |
| v
| +---------------+--------+----------+-------+
| | drop_in_place | size | method_1 | ... |
| +---------------+--------+----------+-------+
v
+---------+---------+--------+
| field_1 | field_2 | ... |
+---------+---------+--------+
And the C++ version:
Foo* (Pointer to derived object)
|
v
+--------+---------+---------+--------+
| Vtable*| field_1 | field_2 | ... |
+--------+---------+---------+--------+
|
v
+----------+-----+
| method_1 | ... |
+----------+-----+
You draw much better than I do . I meant the same though. Once again, thank you for your time and explanation.
What are the pros and cons of doing it this way vs the C++ way? Off the top of my head...
[pro C++] A vector of objects in smaller, 64bits per element.
[pro Rust] You can attach dynamic behavior to types that didn't have it originally, so you're free from the class hierarchy.
(I haven't really used Rust yet. I'm reading the docs playing with it.)
One of the goals of Rust's design in general is to not force the runtime cost of a feature onto code/programs that don't use that feature. In this case, space is only allocated for a vtable pointer in places that are actually using dynamic dispatch; the C++ version, in contrast, stores a copy of the vtable pointer inside every instance whether or not dynamic dispatch is actually being used.
Here's a good comparison of different approaches. It doesn't talk much about Rust except to contrast with the others, but still useful.
This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.