Here are my underlying assumptions of how Rust's methods work:
foo.method();
where method
is defined as method(&self)
and foo
is an instance of Foo,
is the same as
Foo::method(&foo);
My understanding of Trait Objects is a struct with two void pointers, one to the data, and another to the function pointers (vtable)
A polymorphic function that takes in a Trait Object and calls a method on that Trait object will compile down to looking at the offset of the method in the Trait Object and passing in the data pointer
But what if the method moves the instance? Correct me if I am wrong, but to call a virtual move method, the function would have to push the actual data stored inside the Trait Object onto the stack instead of just the data pointer. The data size obviously cannot be known at compile-time, so what is going on here? Is this a VLA sort of situation, or do I misunderstand how a move works?
The answer is very simple - it's impossible to call the self-consuming method on trait object.
The key words are object safety. Essentially, this property combines two requirements:
self
through some kind of indirection;To see this in more detail, let's actually try to code something and ask compiler's opinion. First of all, just trying to define the trait:
trait Consumer {
fn consume(self);
}
Compiler is already unhappy:
error[E0277]: the size for values of type `Self` cannot be known at compilation time
--> src/lib.rs:2:16
|
2 | fn consume(self);
| ^^^^ doesn't have a size known at compile-time
|
help: consider further restricting `Self`
|
2 | fn consume(self) where Self: Sized;
| ^^^^^^^^^^^^^^^^^
help: function arguments must have a statically known size, borrowed types always have a known size
|
2 | fn consume(&self);
| ^
OK, we can be even more conservative then the compiler's advice and add the restriction on trait. Then, add a stub for trait object creation:
trait Consumer where Self: Sized {
fn consume(self);
}
fn main() {
let _: Box<dyn Consumer> = todo!();
}
Now, the error is slightly more complex:
error[E0038]: the trait `Consumer` cannot be made into an object
--> src/main.rs:6:12
|
6 | let _: Box<dyn Consumer> = todo!();
| ^^^^^^^^^^^^^^^^^ `Consumer` cannot be made into an object
|
note: for a trait to be "object safe" it needs to allow building a vtable to allow the call to be resolvable dynamically; for more information visit <https://doc.rust-lang.org/reference/items/traits.html#object-safety>
--> src/main.rs:1:28
|
1 | trait Consumer where Self: Sized {
| -------- ^^^^^ ...because it requires `Self: Sized`
| |
| this trait cannot be made into an object...
There is one workaround, however: it's not necessary to restrict the whole trait - just the offending method, as we were told from the start. Moving where
clause, as here:
trait Consumer {
fn consume(self) where Self: Sized;
}
...makes the code above compile.
Now, what about actually using this trait object? Let's implement it, for example, for unit type, and use this from main
:
trait Consumer {
fn consume(self) where Self: Sized;
}
impl Consumer for () {
fn consume(self) {}
}
fn main() {
let consumer: Box<dyn Consumer> = Box::new(());
consumer.consume();
}
Another compiler error!
error: the `consume` method cannot be invoked on a trait object
--> src/main.rs:11:14
|
2 | fn consume(self) where Self: Sized;
| ----- this has a `Sized` requirement
...
11 | consumer.consume();
| ^^^^^^^
Again, the restriction we've placed on method forbids the code which would make no sense if it compiled.