As it is now, you have to provide is as an actual method:
trait Foo: Bar {
fn foo_method(&self);
fn as_bar(&self) -> &dyn Bar;
}
As it is now, you have to provide is as an actual method:
trait Foo: Bar {
fn foo_method(&self);
fn as_bar(&self) -> &dyn Bar;
}
I believe the terms used in the Rust community for this problem is "trait object upcasting" or "supertrait upcasting". A workaround is shown in this StackOverflow Answer.
Just a small note: that is not a dynamic_cast
in C++, but a static_cast
. Here the C++ version of your example.
I think this is a hack but a viable workaround.
Is there any syntactic sugar to do auto-casting without explicitly providing this converter function?
There's some clever trick with adding an extra trait with a blanket impl, but I don't remember it.
Thanks! Glad to know this compiler explorer! Every time I have to use -masm=intel -S
to view asm. This is a nice interface!
Actually, as I discovered, Foo is not a Bar and I update the question. I make some modifications on your code, see here. In my code, I use pure C pointer cast, which is definitely unsafe from Rust's perspective.
It sounds like Rust only provides static-polymorphism (see term 2), and no runtime up- or down-cast is provided as dynamic_cast
does in C++.
In this way, static_cast
cannot be performed since these 2 types are not in a shared inheritance chain.
As you said in your edit, Rust is not designed for inheritance and classic object-oriented approaches. There are valid reasons for that, but this is another story.
However, this does not mean that Rust does not support runtime polymorphism. You can use Any
to use runtime polymorphism with erased types, and you can also try downcast_rs to emulate an OO behavior, however I do not recommend doing that. Rust is based on different paradigms, and you will probably find yourself fighting with the borrow checker if you try to use approaches that do not fit very well
Another small note on you example: the C++ code on godbold does not perform a dynamic cast, what you wrote is a reinterpret_cast with C notation. This can lead to undefined behavior pretty easily if misused. In fact, this (unfortunately) compiles and run... sort of, because it is UB.
On the other hand, in this example the code really uses a dynamic cast, throwing an exception when things are not fine, without triggering UB. However you can see that it has a serious cost -- look at the ASM code.
Yes, I didn't use dynamic_cast
but reinterpret_cast
. I added the dynamic_cast
to the code. The compiler can figure out through the inheritance tree at runtime (yes, there's runtime type reflection overhead). This type of reinterpret_cast
or C style cast really needs developers' discretion and do human type-check, which is mostly unreliable. I can see why Rust forbids it.
Yeah, I find the reason for Rust's choice of generic polymorphism instead of OOP classic inheritance polymorphism. Well, to me, it should be called interface oriented paradigm, which is really similar to Java's interface design except Java's interface can access private members by inheritance but Rust doesn't allow that because its interface implementation is an aggregation of the underlying object instance.
Thanks a lot! I really enjoy this community!
The clever trick that @alice refers to is this,
// blanket impl for all sized types, this allows for a very large majority of use-cases
impl<T: Bar> AsBar for T {
fn as_bar(&self) -> &dyn Bar { self }
}
// a helper-trait to do the conversion
trait AsBar {
fn as_bar(&self) -> &dyn Bar;
}
// note that Bar requires `AsBar`, this is what allows you to call `as_bar`
// from a trait object of something that requires `Bar` as a super-trait
trait Bar: AsBar {
fn bar_method(&self) {
println!("this is bar");
}
}
// no change here
trait Foo: Bar {
fn foo_method(&self) {
println!("this is foo");
}
}
// now this works
fn foo(foo: &dyn Foo) -> &dyn Bar {
foo.as_bar()
}
Now I know let foo: &dyn Foo = bar
is using static_cast
from Bar
trait object to Foo
trait object and this will fail because they are different types. But how could rust compile tell?
Would the rust compiler generate trait-specific code behind dyn Foo
?
I checked this doc, it says this
pub struct TraitObject {
pub data: *mut (),
pub vtable: *mut (),
}
is ABI compatible representation of trait object, which means this is not the underlying representation in rust.
Would dyn Foo
generate pub struct TraitObjectFoo
and dyn Bar
generate TraitObjectBar
, or something like that to differentiate they are 2 different trait object type?
Yes, something like that. Trait objects are stored like so
// for example, the vtables for i32 and Vec<i32>
static VTABLE_FOO_FOR_I32: VTableFoo = VTableFoo { ... };
static VTABLE_FOO_FOR_VEC_I32: VTableFoo = VTableFoo { ... };
pub struct TraitObjectFoo {
data: *mut (),
vtable_ptr: &VTableFoo,
}
pub struct VTableFoo {
layout: Layout,
drop_in_place: unsafe fn(*mut ()),
// methods
foo_method: fn(*mut ()),
bar_method: fn(*mut ()),
as_bar: fn(*mut ()) -> &dyn Bar,
}
This VTableFoo
is generated for each and every that that implements Foo
(modulo optimizations that strip unused impls). Then the fat pointer refers to the vtable by reference. storing something like (data_ptr, &VTABLE_FOO_FOR_I32)
. Note: the layout of VTableFoo
is not related to the layout of VTableBar
in any way, shape, or form.
Thank a lot! Save me a lot of time learning Rust! Much appreciated!
One more thing I almost forgot about, the vtable for a given trait implementation may be generated multiple times. This means that pointer equality may spuriously return false if the two vtables were generated in different translation units, even if they should otherwise be equal!
That's interesting to know. I guess there might be duplicate definitions in the anonymous scope within a translation unit
// translation unit: foo.cc
// C++ code
namespace {
struct VTableFoo {
unsigned Layout;
void* drop_in_place;
// methods
....
}
dummy_vtable = VTableFoo{ /* init */};
} // anonymous
// refer this definition in same foo.cc
TraitObjectFoo dummy { /* data address */, &dummy_vtable};
or just within that unit without external reference (just remove the namespace).
Just curious: why not use single definition across different translate units? Is there any specific reason to generate multiple definitions among different units? Like, to speed up the compilation?
This may happen, for example, when you use a DLL. The DLL has one version of the implementation, and your current build may have another version of the same trait impl. But even if you statically link everything I think we may generate duplicate vtables to avoid synchronizing across translation units, which improves performance (last I asked, these were the answers).
Globally unique statically generated vtable is generally not possible. Golang solved this issue by generating and caching vtable object at runtime. But I don't think it's viable for the Rust.
Got it! One more thing, in your example (10th floor)
// a helper-trait to do the conversion
trait AsBar {
fn as_bar(&self) -> &dyn Bar;
}
// note that Bar requires `AsBar`, this is what allows you to call `as_bar`
// from a trait object of something that requires `Bar` as a super-trait
trait Bar: AsBar {
fn bar_method(&self) {
println!("this is bar");
}
}
// no change here
trait Foo: Bar {
fn foo_method(&self) {
println!("this is foo");
}
}
in your answer,
pub struct VTableFoo {
layout: Layout,
drop_in_place: unsafe fn(*mut ()),
// methods
foo_method: fn(*mut ()),
bar_method: fn(*mut ()),
as_bar: fn(*mut ()) -> &dyn Bar,
}
I'm a little bit confused. I thought vtable for Foo will only have only one method, which means foo_method
and trait object dyn Foo
can only call foo_method
and it cannot call bar_method
.
Now it seems the vtable should include all methods from siblings. That is to say
Trait A: B, C {}
// flat relation graph
// B, C
// ^
// A
Table for trait object A should have all methods from B, C, which makes the vtable really flat (maybe large in size).
Am I right on this?
// c++: foo.cc
struct C {
virtual ~C() = default;
};
struct B : public C {
virtual ~B() = default;
};
struct A : public B {};
int main(int argc, char **argv) { return sizeof(A); }
// hierarchical relation
// C
// ^
// B
// ^
// A
run clang -cc1 -std=c++11 -fdump-record-layouts foo.cc
to check memory layout,
*** Dumping AST Record Layout
0 | struct C
0 | (C vtable pointer)
| [sizeof=8, dsize=8, align=8,
| nvsize=8, nvalign=8]
*** Dumping AST Record Layout
0 | struct B
0 | struct C (primary base)
0 | (C vtable pointer)
| [sizeof=8, dsize=8, align=8,
| nvsize=8, nvalign=8]
*** Dumping AST Record Layout
0 | struct A
0 | struct B (primary base)
0 | struct C (primary base)
0 | (C vtable pointer)
| [sizeof=8, dsize=8, align=8,
| nvsize=8, nvalign=8]
There's only one vtable for A, B, and C. But for Rust, there is a vtable implementation for each trait (e.g., A, B and C).
To me, even though Rust inheritance is not the same as C++, but the vtable layout, in the end, is the same for struct A and trait A. The only difference is that C++ vtable lives inside of each object whereas rust lives in the fat pointer, alongside data pointer.
In the long run, the number of objects is far more than the number of traits, this decision may save some space.
One more question that I have.
How does vtable check which method to be used at runtime if it has many methods in the table? Is the method offset compile-time known? E.g., if foo_method
is called, rust will remember its offset, let's say 3 at compile-time, and at runtime use that index 3 to load the method for foo?
Yeah, the vtable for Foo
does have bar_method
. It's possible that it has it by having a pointer to the vtable for the Bar
impl, but I don't know.
I don't know for certain, but I am pretty sure that it does indeed know the offset in the vtable and hardcodes that offset.
After spending some time learning ASM, here's an example of how vtable works.
struct Bar {
virtual void method1() {
std::puts("method1: this is bar");
}
virtual void method2() {
std::puts("method2: this is bar");
}
virtual ~Bar() {};
};
struct Foo: public Bar {
virtual void method1() {
std::puts("method1: hi, I'm foo");
}
virtual ~Foo() {};
};
int main() {
Bar bar;
Foo foo;
Bar* bar_arr[2];
bar_arr[0] = &bar;
bar_arr[1] = dynamic_cast<Bar*>(&foo);
for (auto ptr: bar_arr) {
ptr->method1();
ptr->method2();
}
return 0;
}
This is intel asm syntax for vtable.
vtable for Foo:
.quad 0
.quad typeinfo for Foo
.quad Foo::method1()
.quad Bar::method2()
.quad Foo::~Foo() [complete object destructor]
.quad Foo::~Foo() [deleting destructor]
vtable for Bar:
.quad 0 // q-word -> 8 bytes, will be used as offset
.quad typeinfo for Bar
.quad Bar::method1()
.quad Bar::method2()
.quad Bar::~Bar() [complete object destructor]
.quad Bar::~Bar() [deleting destructor]
For each class, it carries a table (if there's no override, it will carry the same vtable from the parent).
let's check this line,
for (auto ptr: bar_arr) {
ptr->method1();
ptr->method2();
}
First, it needs to store the vtable address into the stack along with the variable bar
and foo
,
// bar memory layout is a simple vtable pointer, size is 8
mov eax, OFFSET FLAT:vtable for Bar+16
mov QWORD PTR [rbp-56], tax
// same for foo
mov eax, OFFSET FLAT:vtable for Foo+16
mov QWORD PTR [rbp-64], rax
The compiler knows the offset of method1
and method2
in vtable.
mov rax, QWORD PTR [rbp-48] // temp var, store current arr iterator: ptr = bar_arr[0]
mov rax, QWORD PTR [rax]
mov rdx, QWORD PTR [rax]
mov rax, QWORD PTR [rbp-48]
mov rdi, rax // set up parameters for function call
call rdx // call method1 from vtable stored in the object, e.g., bar and foo
mov rax, QWORD PTR [rbp-48]
mov rax, QWORD PTR [rax]
add rax, 8 // method1 + 8 offset -> method2
mov rdx, QWORD PTR [rax]
mov rax, QWORD PTR [rbp-48]
mov rdi, rax
call rdx
a vis for stack: <rbp - number> | c syntax
-24 | ptr (Bar*) | // like counter index i
-32 | bar_arr (Bar**) |
-40 | bar_arr + 2 | // end of bar_arr[]
-48 | temp var |
-56 | bar (a vtable ptr) |
-64 | foo (a vatble ptr) |
-72 | &foo (bar_arr[1]) |
-80 | &bar (bar_arr[0]) |
This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.