Rust Traits vs Inheritance

Rust uses traits but is pure virtual inheritance still better for some cases?

The functions that accept an object with a specific trait that requires specific function(s) must access the functions of the function-traited object somehow. Is a unique instance of a function that accepts a such an object required for each call of the function that has a unique (different) kind of object that implements the trait as the function arg? (Granted, the compiler could take care of this.)

I.e. with inheritance, a single instance of a function can accept a pure virtual base class argument that will work for any dynamically created instance of an object that inherits the pure virtual base class. Do Rust traits support this.

To further clarify by contrast: In C++, for functions that have a template parameter T and corresponding arg, e.g. some_function<typename T>(T t), the compiler generates a unique instance of the function template for each call of some_function that has a unique T. That's why the function templates are typically implemented in header files. I.e. They are "inlined". However, a function that accepts a base class can be implemented in a source file (i.e. inlining not strictly necessary).

Do functions in Rust that accept an object with a specific trait as an arg, require inlining for use with each unique type that implements the trait (similar to the C++ inlining of function templates for each unique T)?

1 Like

Rust monomorphises generic functions. So yes, a copy of your function for each concrete type that you use as the generic parameter in your program will be created by the compiler.

Rust has trait objects for virtual dispatch at run time.

7 Likes

virtual dispatch

Thank you kindly. Would this be like a switch case / pattern matching ? Would this be similar in performance to one level of indirection e.g. vtable?

1 Like

Trait objects use vtables. Pointers (&, &mut, Box, Arc, etc.) to trait objects are wide pointers that consist of (I) a pointer to the object itself and (II) a pointer to a vtable with the methods of the base trait and its supertraits.

7 Likes

Correct me as needed. So Rust supports inheritance under the hood as the compiler deems appropriate, and traits are the syntax? Is that a reasonable assessment?

1 Like

It’s more explicit than that: vtables are used iff methods are dispatched via a dyn Trait type, and the compiler will generate a vtable for each type that is coerced to a dyn Trait instance in order to support that.

Because the vtable pointer is stored in the reference instead of the base type, the decision to generate a vtable or not can be deferred until after the type layout is calculated and possibly even triggered by code in a different crate entirely— That way, only code that interacts with the dynamic dispatch system directly needs to care about or pay the price of virtualization.

Another key difference here is that Rust’s system doesn’t support any kind of implicit layout/field sharing. If you want that, you’ll need to use encapsulation explicitly, either by having each “subtype” include the base type or by the base type taking a generic parameter to represent the extension fields.

8 Likes

...the decision to generate a vtable or not...

One crate per library package? Multiple libraries ==> multiple packages? Multiple library packages part of a sort-of PGO scope? That's a good thing. (Still learning packages/crates/modules).

Yes, I think you are touching on how structs etc. don't have layout by order as C/C++ does but that it can be controlled/specified if needed. Makes sense to allow compiler freedom unless something is needed, e.g. for a memory-mapped hardware device.

So Rust prefers performance by default (a plus) but does support use cases where vtables are optimal e.g. for a call site of a function that has a traited-type arg, and the objects that implement the trait are dynamically created at the call site.

A vtable is not a kind of runtime dispatch that I am concerned about (if only one level of indirection). As long as Rust has no shortcomings there relative to C++, I can understand more details as I go. (In C function pointers can be used as you probably know.)

C++ was inheritance-centric and now supports concepts. Rust seems to be trait-centric but also supports vtables when optimal, from what I am gathering.

Does Rust limit vtables behavior to one level of indirection (similar to a function pointer in a C struct)?

When C++20 modules were introduced, it seemed that libraries were beginning to take the form of precompiled importable modules that were similar to an inlining approach. However, I think that functions in a module may also be in a dll and so the bulk may not always all be in the precompiled module. I.e. the module may contain binary ABI but not always the function definitions (bodies) themselves. From what I am learning about Rust, it seems that C++20 modules mimic the Rust approach to some extent.

C# compiles all sources in a project and compiler has larger scope than just one source file and I think that I see some commonalities with how Rust approaches compilation. I liked the ability in C/C++ to click "Compile File" in the IDE but I think that those days are passing and compilers are looking around a bit more to see what is using what :slight_smile:

This may also relate to compile times in Rust but one can see that it serves a purpose.

You may find interesting and Rust may be a language that is good at addressing these kinds of issues (in which case another big plus for Rust):

1 Like

I also read how trait objects support late-binding (Dr. Alan Kay). I think that I am all set!

Thank you @2e71828 also.

I marked your answer as solution and all of your answers were part of it, thank you kindly!

You may comment on my additional questions/comments if you'd like but I have been pointed in the right direction.

1 Like

In the end, it's ultimately the programmer's decisionÂą, but Rust's design certainly encourages having only a single level of indirection:

  • If you have a Box<MyStruct>, you can turn it into a Box<dyn MyTrait> which will generate an appropriate single-indirection vtable at compile time.
  • You can't turn a Box<dyn MyTrait> into a Box<dyn MyOtherTrait> directly, which would be the most direct way to get a double-indirection vtable.
  • You can, however, explicitly box it again and turn the resulting Box<Box<dyn MyTrait>> into Box<dyn MyOtherTrait> if there's an appropriate implementation of MyOtherTrait for Box<...>. This will result in a double indirection: First to find the correct implementation of MyOtherTrait::method1, and then again inside that implementation to look up MyTrait::method2 on the inner box's content.

Âą This is roughly analogous to C, which can also have double-indirection if the first function pointer points to a function that looks up and calls another function pointer.

7 Likes

Whether there is more than one level of indirection in the vtables or not[1] is considered an implementation detail as far as I'm aware.

Looks like the approach described here (which has no extra indirection for methods) is still accurate.

When dyn upcasting stabilizes, coercing from Box<dyn SubTrait> to Box<dyn SuperTrait> and the like will be possible.


  1. and the exactly design of vtables more generally ↩︎

7 Likes