EDIT (20250405): remove the statements that the trait class can contain an associated enum
item, and move it to the "future possibilities" part as it is largely related to something like enum-dispatch.
Summary
Introducetrait class
containing to support single inheritance.
Motivation
It is hard to design a GUI framework in Rust without inheritance. E.g., rendering objects are usually organized as a tree, and they share lots of properties and behaviours but vary with one another. Rust now has excellent trait
for abstraction over different types, but may lack reusability or extendability (e.g. it is not easy to extend from a trait interface with default implementations and keep most of its behaviours but override only a few of them).
Moreover, It is not easy to construct custom virtual tables and use them in Box
, Rc
, etc. as they don't support pointers with custom metadata.
It has been greatly debated whether to introduce inheritance in Rust. I would like to vote yes but it definitely should be done in a native rusty way.
Guided-level explanation
This (Pre-)RFC is to introduce trait class
that is a combination of a trait and an ADT.
Example: trait class
as a type
trait class
can be used like a struct
type:
pub trait class Person {
// `struct` specify the data structure contained by this trait class,
// besides `struct`, `enum` is also allowed (`union` is not allowed).
struct {
pub name: String,
pub age: u32,
}
}
mod adt {
// It can be used samely as the following type
pub struct Person {
pub name: String,
pub age: u32,
}
}
The above trait class is a simple case with only a struct
, and there is no difference with the struct
inside when it is used as a type:
let person = Person { name: "Bob".to_string(), age: 38 };
let person = adt::Person { name: "Bob".to_string(), age: 38 };
Person
has the same memory layout with adt::Person
as it doesn't have a superclass.
Example: trait class
as a trait (bound)
trait class
are also very similar to the trait
:
pub trait class Animal {
fn make_sound(&self);
}
mod _trait {
pub trait Animal {
fn make_sound(&self);
}
}
The above trait class can be used similarly like a trait:
fn foo<T: Animal>(animal: T) {
animal.make_sound();
}
fn bar<T: _trait::Animal>(animal: T) {
animal.make_sound();
}
or by dynamic dispatch:
fn foo(animal: &dyn Animal) {
animal.make_sound();
}
fn bar(animal: &dyn _trait::Animal) {
animal.make_sound();
}
This is on the public interface side of the trait class
. However, trait class
is different from trait
on the implementation side.
To "implement" a trait class
, you have to inherit from it instead of impl
it.
// pub trait class Animal { ... }
pub trait Dog : Animal {
// it contains an implicit `struct {}` definition
override fn make_sound(&self) {
println!("Wolf!");
}
}
mod _trait {
// pub trait Animal { ... }
pub struct Dog {}
impl Animal for Dog {
override fn make_sound(&self) {
println!("Wolf!");
}
}
}
Then, the inherited trait class can be used similarly like the implemented type:
// `use` is not needed to call a trait class method, as it belongs to the type `Dog` itself.
let dog = Dog {};
dog.make_sound();
// `use _trait::Animal` is needed to call a trait method.
let dog = _trait::Dog {};
dog.make_sound();
Example: using trait class
to express inheritance
Data and behaviours can be easily reused via trait class
:
pub trait class Person {
struct {
name: String,
age: u32,
}
fn greet(&self) {
println!("Hello, my name is {}. I'm {} years old.", self.name, self.age);
}
}
// `Employ` inherits data and behaviors from its superclass `Person`
pub trait class Employee: Person {
struct {
company: String,
}
// Using `override fn`, we can override its superclass's behaviour.
override fn greet(&self) {
// We can reuse its superclass's behavior via `super.method(...)` syntax
super.greet();
println!("I work for {}.", self.company);
}
}
pub trait class Developer: Employee {
override fn greet(&self) {
super.greet();
println!("I'm a developer.");
}
}
Constructing the trait class objects:
let person = Person { name: "Bob".to_string(), age: 38 };
person.greet();
let employee = Employee {
name: "Alice".to_string(),
age: 45,
company: "Facebook".to_string(),
};
employee.greet();
let developer = Developer { ..employee };
developer.greet();
// Output:
/*
Hello, my name is Bob. I'm 38 years old.
Hello, my name is Alice. I'm 45 years old.
I work for Facebook.
Hello, my name is Alice. I'm 45 years old.
I work for Facebook.
I'm a developer.
*/
Trait classes support both static and dynamic dispatch like traits:
// Static dispatch over trait class `Person`
fn hello<T: Person>(person: &T) {
person.greet();
}
fn hello_dyn(person: &dyn Person) {
person.greet();
}
let developer = Developer {
name: "Bob".to_string(),
age: 38,
company: "Intel".to_string()
};
hello(&developer);
hello_dyn(&developer);
// Both above prints:
/*
Hello, my name is Bob. I'm 38 years old.
I work for Intel.
I'm a developer.
*/
Notice that when the trait class Person
is used as a type, it is not polymorphic:
fn hello_non_poly(person: &Person) {
person.greet();
}
hello_non_poly(&developer);
// It only prints the first line from `Person::greet`.
/*
Hello, my name is Bob. I'm 38 years old.
*/
Reference-level explanation
The trait class consists of the header part (approximately the same as a trait) and a block { }
with zero-to-many associated items.
The syntax of the header is like this:
// trait class header
$meta
$vis $(abstract)? trait class $name $(<$generics>)?
: $($superclass)? $(+ $trait_bound)*
$(where $($where_clause,)* )?
The associated items include:
- Zero or one associated struct. Syntax
$meta struct { $($field_def)* }
- Zero-to-many associated types, the same as in a trait.
- Zero-to-many associated constants, the same as in a trait.
- Zero-to-many associated functions:
- required functions that end with
;
, without bodies, the same as in a trait. - provided functions that end with a body block
{ $block_expr }
. Syntax
- required functions that end with
$meta $(override)? fn $($path::)? $name(
$($receiver, )?
$($pat: $ty,)*
) $(-> $ty)?
$(where $($where_clause,)* )?
{
$block_expr
}`
where the keyword override
means it overrides other functions from its superclasses or supertraits.
Constructors are special associated functions that don't have a self
receiver and return type Self
trait class
as a type
In a trait class, we can define an associated struct item. Tuple structs and unit structs are not allowed to be associated struct items. If the user does not specify the associated struct
, there is an implicit empty struct struct {}
by default.
The trait class can inherit from another trait class. Writing trait class A : B
means A
extends from B
, and we say A
is the (direct) subclass of B
and B
the (direct) superclass of A
. Superclass and subclass relationship is transitive: i.e. if A
is the superclass of B
, and B
is the superclass of C
, then A
is also the superclass of C
, vice versa.
Conceptually, every trait class can be treated as a struct
with fields collected from its own and all of its superclass's associated structs. The names of fields must be unique in the trait class itself and its all superclasses (i.e., subclasses cannot override fields or variants of their superclasses).
When a trait class is used as a type, it means a monomorphic type. In contrast, a trait class can also be used as a trait bound or a trait object, which means a polymorphic type.
trait class Base {
struct {
field1: u32,
}
}
trait class Extended : Base {
struct {
// field1: u32, //~ ERROR `field1` duplicated with `Base::field1`
field2: u32,
}
}
can be conceptually viewed as:
#[non_exhaustive]
struct Base {
field1: u32,
}
// A flat struct with all fields from `Base` and `Extended`
#[non_exhaustive]
struct Extended {
field1: u32,
field2: u32,
}
Value construction, field access and pattern matching
Trait classes much resemble normal structs regarding value construction, field access, and pattern matching, except that the special field super
can be used to represent the (direct) superclass during value construction and pattern matching.
When a trait class is used as a trait bounded or a trait object, the pattern matching of its values always requires the non-exhaustive notation ..
in the struct
pattern.
// Value construction
let base = Base { field1: 0 };
let extended = Extended { field1: 0, field2: 0 };
// use the special field `super` explicitly.
let extended = Extended { super: base, field2: 0 };
// Field access
let field1 = extended.field1;
let field2 = extended.field2;
let field3 = extended.field3;
// Pattern matching
let Extended { field1, field2 } = extended;
// use `super` to explicitly ignore fields from its superclass.
let Extended { super: _, field2 } = extended;
// Pattern matching for the polymorphic type
fn f<T: Base>(base: T) {
let Base { field, .. } = base;
}
Abstractness of the trait class
We say a trait class abstract if any of the following holds:
- it contains any associated functions without a default implementation.
- it is explicitly restricted with
abstract
. - it doesn't implement (override) any unimplemented functions from its superclasses or supertraits.
Abstract trait class are not allowed to be used as a type, nor constructing its value (except in the constructors), but can be used as trait bounds or trait objects.
Memory layout
The associated struct of a trait class can be either repr(Rust)
(by default) or repr(C)
.
In any representation (both repr(Rust)
and repr(C)
), the memory layout of any subclass must be compatible with its superclass's, that is:
- the subclass must preserve the offsets of all fields and all variants (including variant fields) from all its superclasses.
For repr(Rust)
, there is no guarantee of the layout of fields and variants except the compatibility rule above.
For repr(C)
, the memory layout of a trait class is the same as the flatten struct with all fields of its own and its superclasses:
- field order is preserved, and all superclass fields are placed before the subclass fields.
- the first field begins at offset 0 if it doesn't have a superclass.
- each field's offset is aligned to the ABI-mandated alignment for that field's type, possibly creating unused padding bits.
- the total size of the struct is rounded up to its overall alignment.
trait class
as a trait bound
The trait class can be used as a trait bound, and of course, it can be used together with other trait bounds (but at most one trait class is allowed in the trait bound).
Assuming TraitClass1
and TraitClass2
are trait classes, and Trait1
and Trait2
are normal traits:
fn f<T: TraitClass1 + Trait1 + Trait2>() {} //~ OK
fn g<T: TraitClass1 + TraitClass2>() {} //~ ERROR at most 1 trait class bound is allowed
fn h(x: impl TraitClass1 + Trait1) //~ OK
fn i(x: impl TraitClass1 + TraitClass2) //~ ERROR at most 1 trait class bound is allowed
fn j() -> impl TraitClass1 + Trait1 //~ OK
fn k() -> impl TraitClass1 + TraitClass2 //~ ERROR at most 1 trait class bound is allowed
Trait bounds can also appear in the definition of the trait class, which is a requirement instead of an providing of such trait bound. All trait bounds must be implemented then the trait class is allowed to used as a type.
trait class Example: fmt::Debug {
struct {
field: u32,
}
fn example(&self) {
println!("{self:?}");
}
}
imp fmt::Debug for Example {
fn debug(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Example").field("field", &self.field).finish()
}
}
Trait bound can also be implemented in-place:
trait class Example: fmt::Debug {
struct {
field1: u32,
}
fn example(&self) {
println!("{self:?}");
}
// implement the `fmt::Debug` trait in-place inside the trait class
// the keyword `override` is required as it is implementing its' supertrait's function
override fn debug(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Example").field("field1", &self.field1).finish()
}
}
Subclasses can override superclass's trait implementation:
trait class Inherited: Example {
struct {
field2: u32,
}
override fn debug(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Inherited")
.field("super", &super)
.field("field2", &self.field)
.finish()
}
}
trait class
as a trait object
When a trait class is dyn-safe (obeying the same rule as normal traits), it can be used as a trait object (naming a trait class object) via the keyword dyn
.
For any trait class TraitClass
:
dyn TraitClass
is unsized and must be used as a reference or pointer (Box
,Rc
, etc.).dyn TraitClass
implementsPointee
where<dyn TraitClass as Pointee>::Metadata
isDynMetadata<dyn TraitClass>
, conceptually a&'static
reference to the vtable of the trait class.TraitClass
implementsUnsize<dyn TraitClass>
so that any reference or pointers ofTraitClass
can be coerced intodyn TraitClass
's.
Vtable of a trait class
Similar to the dyn-safe trait, each dyn-safe trait class also has a vtable. We can simply reuse the vtable layout of the trait object for trait class objects because it just add an associated data item that will not affect its vtable.
Thanks to the stabilized trait object upcasting feature, we can leverage it to support upcasting for trait class objects, too.
Subtyping/Auto-deref/Upcasting rules of trait class objects
In order not to increase the complexity of the Rust's type system, we do not introduce new subtyping for trait classes. We only add some auto-deref and upcasting mechanism.
Due to the guaranteed compatible trait class data layout regarding its superclasses, and reusing the compatible trait object vtable layout regarding its first supertrait, we can safely allow any reference or pointer to the trait class to be coerced to its superclass. We can even support upcasting of the trait class object to its other trait bounds besides the superclass.
For trait class types, we implement Deref
and DerefMut
to its direct superclass type.
trait Trait {}
trait class Base: Trait {}
trait class Extended : Base {}
fn f(x: &dyn Extended) -> &dyn Base { x } //~ OK trait-class-to-trait-class upcast
fn g(x: &mut dyn Base) -> &mut dyn Trait { x } //~ OK trait-class-to-trait-bound upcast
fn h(x: &Extended) -> &Base { x } //~ OK auto deref
fn i(x: &mut Extended) -> &mut dyn Base { x } //~ OK unsize coercion
Common std
traits implementations of trait classes
Debug
:#[derive(Debug)]
can be applied to a trait object when all of its fields and variants are implementedDebug
.Clone
: similar toDebug
but doesn't apply to trait class objects as it requiresSized
.Copy
: similar toClone
.Default
: similar toClone
.PartialOrd
,Ord
,PartialEq
andEq
: compares each field of two trait class types, but doesn't apply to trait class objects because it is not dyn-safe.Hash
: similar toDebug
.Send
: a trait class isSend
iff all of its fields and fields in variants areSend
.Sync
: similar toSend
.Unpin
: similar toSend
andSync
.Freeze
: a trait class isFreeze
when all of its fields and fields in variants do not containUnsafeCell
.
Drawback
- It increases the complexity of the concept
trait
. - It can be quite confusing that
TraitClass
anddyn TraitClass
are different types. - Inheritance may be misused.
Rationale and alternatives
TODO
Prior art
TODO
Unresolved questions
- How to support custom
#[derive]
macros for trait classes? - Should trait classes be allowed to be used as traits that can be implemented by any types?
- Should (checked) downcasting of trait class objects be supported?
Future possibilities
In this (Pre-)RFC, trait class can be dynamically dispatched by the fat-pointered trait object. Sometimes we may also want to use more cache-friendly enum-dispatch strategies. However, enum
is not extendable, i.e. client users cannot add new variants to an enum
defined in a library crate. We may extend the ability of trait classes to support enum-dispatch objects, i.e. using an discriminant value inlined in each trait class object header to indicate which type the instance belongs to, instead of using an extra vtable that requires fat pointers.
For example, assuming the notation #[repr(enum, Int)]
enables a trait class to use enum-dispatch with an optional integer type to represent the discriminant value:
// it allows at most 256 classes to be inherited
#[repr(enum, u8)]
pub trait class Base {
fn f(&self) {
println!("Base");
}
}
trait class Extended1 {
struct { v: u32 }
override fn f(&self) {
super.f();
println!("Extended1");
}
}
trait class Extended2 {
struct { v: u32 }
override fn f(&self) {
super.f();
println!("Extended1");
}
}
then the library crate generates code like
#[repr(u8)]
#[non_exhaustive]
pub enum Base {
Extended1(Extended1),
Extended2(Extended2),
}
impl Base {
fn _f_impl(this: &Self) {
println!("Base");
}
pub fn f(&self) {
match self {
Self::Extended1(extended1) => extended1.f(),
Self::Extended2(extended2) => extended2.f(),
}
}
}
impl Extended1 {
fn _f_impl(this: &Self) {
println!("Extended1");
}
fn f(&self) {
Base::_f_impl(self);
Self::_f_impl(self);
}
}
impl Extended2 {
fn _f_impl(this: &Self) {
println!("Extended2");
}
fn f(&self) {
Base::_f_impl(self);
Self::_f_impl(self);
}
}
When the client code adds a new subclass of Base
, it simply just adds a new variant to Base
. If the discriminant values of the base trait class have been run out (e.g., at most 256 variants can be defined), the compiler reports an error. In other words, the library writers must decide how many discriminates they want to reserve. Usually, 256 of u8
or 65536 of u16
is a reasonable limit for the number of all subclasses.