What makes something a "trait object"?
Asked Answered
N

4

95

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.

Neysa answered 19/12, 2014 at 14:14 Comment(0)
N
126

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

Nyeman answered 19/12, 2014 at 16:27 Comment(11)
Thanks, this seems to be a comprehensive answer. What about creating my own type that can act like a trait object?Neysa
@Shepmaster, types do not "act" like trait objects; it's rather that any pointer to a trait is a trait object, and there can be different kinds of pointers. Box<T> is an owning pointer, Rc<T> is a shared ownership-pointer, Arc<T> is a multithreaded shared ownership-pointer, etc. In principle, each of these can be used to define trait objects, but currently only references and Boxes work for this. So no, right now you can't create custom pointer types which could be used to create trait objects.Dinky
@VladimirMatveev thanks! From your comment and the edit, my current understanding is that a "trait object" is equivalent to &Trait, and then a Box<Trait> or Rc<Trait> can ultimately provide a &Trait.Neysa
@Shepmaster, no, that is not entirely correct. Box<Trait>/possible Rc<Trait> are trait objects themselves too, and they are not converted or provide &Trait.Dinky
@VladimirMatveev at this point in time (Rust 1.4), would you say that something which implements Deref<Target = SomeTrait> would fulfill my question about "creating my own type ..." ?Neysa
This clever answer from someone seems to show how to use Rc<SomeTrait>, so I'm not sure why you say only & and Box work with unsized objects like traits.Neysa
@Neysa when I edited to add the link to the Book I saw that it says "These trait object coercions and casts also work for pointers like &mut T to &mut Foo and Box<T> to Box<Foo>, but that’s all at the moment" so I also added that I think it might not work for Rc and Arc until I'd had the time to double check if they actually do...Nyeman
@VladimirMatveev @Paolo Falabella: After reading the answer and the comments I am confused about the meaning of the term "trait object", and the term for the object that is referenced by the Box or &. Vladimir writes: "Box<Trait> and &Trait are trait objects" Does that mean that it is the pointer-value (Box<Trait> or &Trait) that is called a trait object? (This is counter to my intuition about the term "object".) In that case, what do we call the object to which the Box<Trait> or &Trait points?Pocked
@Pocked I don't think there is a practical difference. The term "trait object" can be applied to both, and usually it does not introduce confusion. I'd say that semantically it does indeed refer more to the value behind the pointer. But if it is needed to strictly disambiguate between the fat pointer and the value it points to, I usually call them "trait object pointer" and "value that the trait object pointer points to".Dinky
> The term "trait object" can be applied to both, and usually it does not introduce confusion. FWIW, I for one was confused by this quite a bit :) The ambiguous usage felt like trait objects = fat pointers to data + vtable, but these fat pointers are also at the same time somehow supposed to be unsized, which doesn't make sense. Fortunately, the Rust reference is currently clear about this: the unsized value dyn Trait itself is a trait object and must be used behind a pointer of some kind (&dyn Trait, Box<dyn Trait> etc.).Bilow
You can’t have a pointer to a trait. Traits are a compile-time construct.Andean
R
9

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

Reform answered 23/7, 2018 at 20:1 Comment(1)
More generally, everything that isn't at object level (aka use Self) makes a trait not object-safe. For example, if your trait has a const member or a function that doesn't have a self as first parameter.Feathers
I
7

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);
}
Isabelleisac answered 28/12, 2020 at 11:54 Comment(2)
Why would someone ever use dynamic dispatch, if static dispatch seems to do everything dynamic dispatch does and there's no runtime overhead?Ideate
@Ideate see this and this.Orelle
O
-1

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.

Orelle answered 1/1, 2023 at 11:47 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.