Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Rust FAQ: Traits -- Construction and Destruction

PART11 -- Traits and Inheritance (Construction and Destruction)

Q66: Why does a trait's default method get called instead of the implementor's?

In Rust, a trait’s default method gets called instead of the implementor’s method when the method is invoked through a trait object (dyn Trait) or when the implementor does not override the default method. This happens because of how Rust resolves method calls, particularly with dynamic dispatch, or because the implementor’s method isn’t defined.

Why this happens:

  • Dynamic Dispatch with Trait Objects: When using a trait object (Box<dyn Trait>, &dyn Trait), Rust uses a vtable to resolve method calls at runtime. If the implementor doesn’t override the trait’s default method, the vtable points to the default implementation.
  • Non-overridden Methods: If a type implements a trait but doesn’t provide its own version of a method with a default implementation, Rust uses the trait’s default method.
  • Method Hiding: If the implementor defines a method with the same name but a different signature, it doesn’t override the trait’s method—it creates a separate method, leading to the trait’s default being called when the trait is in scope (see Q59).

Example:

trait Speak {
    fn speak(&self) -> String {
        String::from("Default sound") // Default implementation
    }
}

struct Dog;

impl Speak for Dog {
    // No override of `speak`
}

struct Cat;

impl Cat {
    fn speak(&self) -> String {
        String::from("Meow!") // Not part of `Speak` trait
    }
}

fn main() {
    let dog: Box<dyn Speak> = Box::new(Dog);
    println!("{}", dog.speak()); // Prints: Default sound (trait's default)

    let cat = Cat;
    println!("{}", cat.speak()); // Prints: Meow! (Cat's method)
    println!("{}", <Cat as Speak>::speak(&cat)); // Prints: Default sound (trait's default)
}
  • Explanation:
    • Dog doesn’t override speak, so the trait’s default method is used.
    • Cat has a speak method, but it’s not tied to the Speak trait (wrong context), so calling speak through the Speak trait uses the default.
    • Using explicit trait syntax (<Cat as Speak>::speak) calls the trait’s default method.

How to ensure the implementor’s method is called:

  • Override the Method: Implement the trait method explicitly for the type:
    #![allow(unused)]
    fn main() {
    impl Speak for Cat {
        fn speak(&self) -> String {
            String::from("Meow!")
        }
    }
    }
  • Use Static Dispatch: Call methods through generics (T: Speak) or the concrete type, not a trait object, to avoid vtable lookup:
    #![allow(unused)]
    fn main() {
    fn call_speak<T: Speak>(item: &T) {
        println!("{}", item.speak()); // Uses implementor’s method
    }
    }
  • Check Signatures: Ensure the implementor’s method matches the trait’s method signature exactly to avoid method hiding (see Q59).

Why this behavior?

  • Rust prioritizes explicitness and safety, ensuring trait methods are resolved predictably.
  • Default methods provide fallback behavior, but implementors must opt-in to override them.
  • Dynamic dispatch relies on the vtable, which uses the default if no override exists.

Q67: Does a struct's Drop implementation need to call a trait's Drop?

No, a struct’s Drop implementation does not need to call a trait’s Drop implementation because traits in Rust, including the Drop trait, are not inherited in the way classes are in other languages. The Drop trait is special: it defines a single method, drop(&mut self), which Rust calls automatically when a value goes out of scope. However, there’s no concept of a “trait’s Drop” that needs to be called—only the struct’s own Drop implementation is invoked.

Key Points:

  • Single Drop Implementation: A struct can only have one Drop implementation, and it’s defined directly for the struct, not inherited or chained from a trait. Implementing Drop for a struct is how you customize its cleanup behavior.
  • No Default Drop: The Drop trait doesn’t provide a default drop method that can be overridden. You either implement Drop for a struct or rely on Rust’s automatic cleanup (which drops owned fields).
  • Trait Objects and Drop: When using a trait object (Box<dyn Trait>), the Drop implementation of the underlying concrete type is called, not a trait-level Drop. The trait itself doesn’t define cleanup behavior.

Example:

struct Resource {
    name: String,
}

impl Drop for Resource {
    fn drop(&mut self) {
        println!("Dropping resource: {}", self.name);
    }
}

trait MyTrait {
    fn use_resource(&self);
}

impl MyTrait for Resource {
    fn use_resource(&self) {
        println!("Using resource: {}", self.name);
    }
}

fn main() {
    let resource = Resource { name: String::from("Data") };
    resource.use_resource(); // Prints: Using resource: Data
    // When `resource` goes out of scope, `Resource`'s `drop` is called
} // Prints: Dropping resource: Data
  • Explanation: The Drop implementation for Resource is called automatically when resource goes out of scope. MyTrait has no Drop behavior to call, and there’s no need to invoke anything from the trait.

With Trait Objects:

fn main() {
    let resource: Box<dyn MyTrait> = Box::new(Resource { name: String::from("Data") });
    resource.use_resource(); // Prints: Using resource: Data
} // Prints: Dropping resource: Data (calls `Resource`'s `Drop`)
  • The Drop implementation of the concrete type (Resource) is called, not anything tied to MyTrait.

When to customize Drop:

  • Implement Drop for a struct to clean up resources (e.g., closing files, releasing network connections).
  • You don’t need to worry about “calling a trait’s Drop” because traits don’t define cleanup behavior in this way.

Best Practice:

  • Implement Drop only for the struct that owns resources needing cleanup.
  • Let Rust handle dropping owned fields (like String in Resource) automatically unless custom logic is needed.
  • For trait objects, ensure the concrete type’s Drop implementation handles all necessary cleanup.