0
votes

Update TL;DR: references to redirect:
Abstraction without overhead: traits in Rust
What makes something a “trait object”?

In C++, we can use class inheritance to implement interface polymorphism:

class Base {
public:
    virtual void func(int) = 0;
};

class DerivedA : Base {
public:
    void func(int) override {
        // specific behavior of DerivedA
    }
};

class DerivedB : Base {
public:
    void func(int) override {
        // specific behavior of DerivedB
    }
};

But virtual functions come with some performance cost. Sometimes what concerned is the different behaviors of the same interface (the func method in this example), and performance is preferred over dynamic dispatch. In these cases, Base is used as a skeleton only. In C++, inheritance without virtual also works:

class Base {
public:
    void func(int);
};

class DerivedA : Base {
public:
    void func(int) {
        // specific behavior of DerivedA
    }
};

class DerivedB : Base {
public:
    void func(int) {
        // specific behavior of DerivedB
    }
};

Is there something equivalent to this in Rust? I know traits in Rust are quite similar, but the use of trait is always virtual. Dynamic dispatch is not necessary here.

Edit: Thank you all who give comments and criticisms. I misunderstand about trait. Traits can be statically dispatched, and also can be dynamically dispatched, depending on your need.

This question was not described well at start, maybe it is more about "how to implement and use an interface, without dynamic dispatch in Rust". Personally I come from C++ background and I am reading Inside the C++ Object Model by Stanley B. Lippman recently. This question also proves that use the C++ thought directly into Rust is not a good method when consider trait, polymorphism features of Rust.

2
trait is always virtual?Mansoor
Where have you heard that "trait is always virtual"?Ted Klein Bergman
A trait method call will only be virtual when it absolutely has to, but it's definitely not always virtual. Rust even has a concept of object-safe trait, and if a trait isn't object safe, those methods can't be called virtually. Same for generic trait methods.mcarton
you are fighting over something that doesn't make sense, a virtual simply doesn't compare to anything rusty, a virtual function in C++ could be statically dispatched depend on compiler context etc.Stargateur
How do you use this version "without virtual"? I got feeling it does't work as you expect! In scenario where it could work virtual call will be discarded by compiler. The only case where it could work in reasonable way is a template.Marek R

2 Answers

3
votes

In Rust, the behavior you described could be implemented with traits.

Traits use static dispatch when used with generics. They are not "always virtual". Rust generics are a zero cost abstraction, meaning that there is no runtime cost. A trait only uses dynamic dispatch when it is made into a trait object, which is explicitly specified using the dyn keyword. This quote from a Rust blog post sums it up:

Rust's traits provide a single, simple notion of interface that can be used in both styles, with minimal, predictable costs.

There are many different ways to model the behavior you outlined with traits. Traits are used to identify shared behavior, so you can choose whichever way makes more sense for your use case.

One solution would be to implement that same trait for different structs composed of each other. Another would be to have two traits (A and B), both with a call method:

pub trait A {
    fn call(num: u8);
}

pub trait B {
    fn call(num: u8);
}

You can create a regular struct called Base:

pub struct Base {}

And implement both traits for Base, with behavior specific to each trait:

impl A for Base {
    fn call(i: u8) {
        println!("call from Trait A: {}", i)
    }
}

impl B for Base {
    fn call(i: u8) {
        println!("call from Trait B: {}", i)
    }
}

You can also have a regular call method for Base:

impl Base {
    fn call(i: u8) {
        println!("call from Base: {}", i)
    }
}

Now you can call the different call methods for Base. Because Base has multiple associated methods with the same name, you have to use full qualified syntax:

Base::call(12);        // => call from Base: 12
<Base as A>::call(12); // => call from Trait A: 12
<Base as B>::call(12); // => call from Trait B: 12
2
votes

Rust traits are not "always virtual". They are only virtual when using trait objects (dyn keyword) which is usually not necessary.

Example:

// not "virtual"
fn foo(obj: impl MyTrait) {}
// equivalent to:
fn foo<T: MyTrait>(obj: T) {}

// but this is "virtual"
fn foo(obj: &dyn MyTrait) {}

Note that the type in the "virtual" case is a ref (&) because the compiler cannot know the size of the argument at compile time.

Edit: When using impl or generics, a copy of the function is created at compile time for every type that is used. When using dynamic dispatch, only one copy of the function is created.