Recent Rust changes have made "trait objects" more prominent to me, but I only have a nebulous grasp of what actually makes something into a trait object. One change in particular is the upcoming change to allow trait objects to forward trait implementations to the inner type.
Given a trait Foo
, I'm pretty sure that Box<Foo>
/ Box<dyn Foo>
is a trait object. Is &Foo
/ &dyn Foo
also a trait object? What about other smart-pointer things like Rc
or Arc
? How could I make my own type that would count as a trait object?
The reference only mentions trait objects once, but nothing like a definition.
You have trait objects when you have a pointer to a trait.
Box
, Arc
, Rc
and the reference &
are all, at their core, pointers. In terms of defining a "trait object" they work in the same way.
"Trait objects" are Rust's take on dynamic dispatch. Here's an example that I hope helps show what trait objects are:
// define an example struct, make it printable
#[derive(Debug)]
struct Foo;
// an example trait
trait Bar {
fn baz(&self);
}
// implement the trait for Foo
impl Bar for Foo {
fn baz(&self) {
println!("{:?}", self)
}
}
// This is a generic function that takes any T that implements trait Bar.
// It must resolve to a specific concrete T at compile time.
// The compiler creates a different version of this function
// for each concrete type used to call it so &T here is NOT
// a trait object (as T will represent a known, sized type
// after compilation)
fn static_dispatch<T>(t: &T)
where
T: Bar,
{
t.baz(); // we can do this because t implements Bar
}
// This function takes a pointer to a something that implements trait Bar
// (it'll know what it is only at runtime). &dyn Bar is a trait object.
// There's only one version of this function at runtime, so this
// reduces the size of the compiled program if the function
// is called with several different types vs using static_dispatch.
// However performance is slightly lower, as the &dyn Bar that
// dynamic_dispatch receives is a pointer to the object +
// a vtable with all the Bar methods that the object implements.
// Calling baz() on t means having to look it up in this vtable.
fn dynamic_dispatch(t: &dyn Bar) {
// ----------------^
// this is the trait object! It would also work with Box<dyn Bar> or
// Rc<dyn Bar> or Arc<dyn Bar>
//
t.baz(); // we can do this because t implements Bar
}
fn main() {
let foo = Foo;
static_dispatch(&foo);
dynamic_dispatch(&foo);
}
For further reference, there is a good Trait Objects chapter of the Rust book
Short Answer: You can only make object-safe traits into trait objects.
Object-Safe Traits: Traits that do not resolve to concrete type of implementation. In practice two rules govern if a trait is object-safe.
Any trait satisfying these two rules can be used as trait objects.
Example of trait that is object-safe can be used as trait object:
trait Draw {
fn draw(&self);
}
Example of trait that cannot be used as trait object:
trait Draw {
fn draw(&self) -> Self;
}
For detailed explanation: https://doc.rust-lang.org/book/second-edition/ch17-02-trait-objects.html
Trait objects are the Rust implementation of dynamic dispatch. Dynamic dispatch allows one particular implementation of a polymorphic operation (trait methods) to be chosen at run time. Dynamic dispatch allows a very flexible architecture because we can swap function implementations out at runtime. However, there is a small runtime cost associated with dynamic dispatch.
The variables/parameters which hold the trait objects are fat pointers which consists of the following components:
struct Point {
x: i64,
y: i64,
z: i64,
}
trait Print {
fn print(&self);
}
// dyn Print is actually a type and we can implement methods on it
impl dyn Print + 'static {
fn print_traitobject(&self) {
println!("from trait object");
}
}
impl Print for Point {
fn print(&self) {
println!("x: {}, y: {}, z: {}", self.x, self.y, self.z);
}
}
// static dispatch (compile time): compiler must know specific versions
// at compile time generates a version for each type
// compiler will use monomorphization to create different versions of the function
// for each type. However, because they can be inlined, it generally has a faster runtime
// compared to dynamic dispatch
fn static_dispatch<T: Print>(point: &T) {
point.print();
}
// dynamic dispatch (run time): compiler doesn't need to know specific versions
// at compile time because it will use a pointer to the data and the vtable.
// The vtable contains pointers to all the different different function implementations.
// Because it has to do lookups at runtime it is generally slower compared to static dispatch
// point_trait_obj is a trait object
fn dynamic_dispatch(point_trait_obj: &(dyn Print + 'static)) {
point_trait_obj.print();
point_trait_obj.print_traitobject();
}
fn main() {
let point = Point { x: 1, y: 2, z: 3 };
// On the next line the compiler knows that the generic type T is Point
static_dispatch(&point);
// This function takes any obj which implements Print trait
// We could, at runtime, change the specfic type as long as it implements the Print trait
dynamic_dispatch(&point);
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With