What makes something a "trait object"?

19.7k views Asked by At

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.

4

There are 4 answers

11
Paolo Falabella On BEST ANSWER

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

0
at54321 On

This question already has good answers about what a trait object is. Let me give here an example of when we might want to use trait objects and why. I'll base my example on the one given in the Rust Book.

Let's say we need a GUI library to create a GUI form. That GUI form will consist of visual components, such as buttons, labels, check-boxes, etc. Let's ask ourselves, who should know how to draw a given component? The library or the component itself? If the library came with a fixed set of all the components you might ever need, then it could internally use an enum where each enum variant represents a single component type and the library itself could take care of all the drawing (as it knows all about its components and how exactly they should be drawn). However, it would be much better if the library allowed you to also use third-party components or ones that you wrote by yourself.

In OOP languages like Java, C#, C++ and others, this is typically done by having a component hierarchy where all components inherit a base class (let's call it Component). That Component class would have a draw() method (which could even be defined as abstract, so as to force all sub-classes to implement that method).

However, Rust doesn't have inheritance. Rust enums are very powerful, as each enum variant can have different types and amounts of associated data, and they are often used in situations where you'd use inheritance in a typical OOP language. An important advantage of using enums and generics in Rust is that everything is known at compile time, which means you don't need to sacrifice performance (no need for things like vtables). But in some cases, as in our example, enums don't provide enough flexibility. The library needs to keep track of components of different type and it needs a way to call methods on components that it doesn't even know about. That's generally known as dynamic dispatch and as explained by others, trait objects are the Rust way of doing dynamic dispatch.

1
Vinod Patel On

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.

  1. The return type isn’t Self.
  2. There are no generic type parameters.

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

2
Willem van der Veen On

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:

  • pointer to the object in memory
  • pointer to that object’s vtable, a vtable is a table with pointers which point to the actual method(s) implementation(s).

Example

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);
}