Understanding Trait Objects and Trait-to-Trait Relationships in Rust
2 min read

Understanding Trait Objects and Trait-to-Trait Relationships in Rust

During a recent coding session, I encountered an interesting problem that led us to explore the depths of Rust's trait system. The journey revealed some important distinctions between Rust's approach and traditional OOP inheritance.

The Initial Problem

I started with code that attempted to convert between two trait objects:

trait Cryptor {
    // Cryptor-specific methods
}

trait Finalizer {
    // Finalizer-specific methods
}

impl Finalizer for dyn Cryptor {}  // Implementing Finalizer for Cryptor trait object

// Attempting to convert &dyn Cryptor to &dyn Finalizer
pub fn cryptor(mut self, cryptor: &dyn Cryptor) -> Result<Self> {
    self.finalizer.add(cryptor as &dyn Finalizer);  // This failed!
    Ok(self)
}

This code failed with the error:

error[E0605]: non-primitive cast: `&dyn Cryptor` as `&dyn Finalizer`

The Journey of Understanding

Second Attempt: Box<dyn Trait>

I then discovered that using Box<dyn Trait> made things work:

impl Finalizer for Box<dyn Cryptor> {}

pub fn cryptor(mut self, cryptor: &Box<dyn Cryptor>) -> Result<Self> {
    self.finalizer.add(cryptor);
    Ok(self)
}

impl Finalizer for Box<dyn Cryptor>{}

This worked, but why?

The Key Insights

Trait-for-Trait Implementation Reality

When we write:

trait A {}
trait B {}
impl B for dyn A {}

What this actually does is automatically implement trait B for any concrete type that implements trait A. It does NOT create a conversion path between trait objects.

Trait Objects and Type Erasure

A trait object (dyn Trait) erases the concrete type information, keeping only the vtable for that specific trait's methods. Even if the original type implemented multiple traits, that information is lost in the trait object.

Rust Traits vs OOP Inheritance

Unlike C++ or Java where inheritance creates an "is-a" relationship allowing upcasting, Rust's trait bounds work differently. When we write TraitB: TraitA, we're saying "to implement TraitB, you must also implement TraitA" - it's a constraint, not an inheritance relationship.

Why Box<dyn Trait> Works

The Box<dyn Trait> approach works because we're implementing Finalizer for a concrete sized type (Box<dyn Cryptor>), not trying to convert between trait objects directly.

Lessons Learned

  1. Trait objects are fundamentally different from inheritance-based polymorphism in other languages.
  2. Implementing a trait for another trait (impl TraitB for dyn TraitA) creates a blanket implementation for concrete types, not a conversion path between trait objects.
  3. When working with trait objects, we need to be mindful of type erasure and the limitations it imposes.
  4. Using Box<dyn Trait> can provide a way to work with trait objects when direct trait-to-trait conversion isn't possible.

This exploration highlighted the unique aspects of Rust's trait system and how it differs from traditional OOP inheritance. Understanding these differences is crucial for writing idiomatic and effective Rust code.


✨ This blog was written by AI! 🤖