I have a trait with two associated functions:
trait WithConstructor: Sized { fn new_with_param(param: usize) -> Self; fn new() -> Self { Self::new_with_param(0) } }
Why does the default implementation of the second method (new()
) force me to put the Sized
bound on the type? I think it's because of the stack pointer manipulation, but I'm not sure.
If the compiler needs to know the size to allocate memory on the stack, why does the following example not require Sized
for T
?
struct SimpleStruct<T> { field: T, } fn main() { let s = SimpleStruct { field: 0u32 }; }
The Sized trait in Rust is an auto trait and a marker trait. Auto traits are traits that get automatically implemented for a type if it passes certain conditions. Marker traits are traits that mark a type as having a certain property.
A trait object is an opaque value of another type that implements a set of traits. The set of traits is made up of an object safe base trait plus any number of auto traits. Trait objects implement the base trait, its auto traits, and any supertraits of the base trait.
A trait in Rust is a group of methods that are defined for a particular type. Traits are an abstract definition of shared behavior amongst different types. So, in a way, traits are to Rust what interfaces are to Java or abstract classes are to C++. A trait method is able to access other methods within that trait.
dyn is a prefix of a trait object's type. The dyn keyword is used to highlight that calls to methods on the associated Trait are dynamically dispatched. To use the trait this way, it must be 'object safe'. Unlike generic parameters or impl Trait , the compiler does not know the concrete type that is being passed.
As you probably already know, types in Rust can be sized and unsized. Unsized types, as their name suggests, do not have a size required to store values of this type which is known to the compiler. For example, [u32]
is an unsized array of u32
s; because the number of elements is not specified anywhere, the compiler doesn't know its size. Another example is a bare trait object type, for example, Display
, when it is used directly as a type:
let x: Display = ...;
In this case, the compiler does not know which type is actually used here, it is erased, therefore it does not know the size of values of these types. The above line is not valid - you can't make a local variable without knowing its size (to allocate enough bytes on the stack), and you can't pass the value of an unsized type into a function as an argument or return it from one.
Unsized types can be used through a pointer, however, which can carry additional information - the length of available data for slices (&[u32]
) or a pointer to a virtual table (Box<SomeTrait>
). Because pointers always have a fixed and known size, they can be stored in local variables and be passed into or returned from functions.
Given any concrete type you can always say whether it is sized or unsized. With generics, however, a question arises - is some type parameter sized or not?
fn generic_fn<T>(x: T) -> T { ... }
If T
is unsized, then such a function definition is incorrect, as you can't pass unsized values around directly. If it is sized, then all is OK.
In Rust all generic type parameters are sized by default everywhere - in functions, in structs and in traits. They have an implicit Sized
bound; Sized
is a trait for marking sized types:
fn generic_fn<T: Sized>(x: T) -> T { ... }
This is because in the overwhelming number of times you want your generic parameters to be sized. Sometimes, however, you'd want to opt-out of sizedness, and this can be done with ?Sized
bound:
fn generic_fn<T: ?Sized>(x: &T) -> u32 { ... }
Now generic_fn
can be called like generic_fn("abcde")
, and T
will be instantiated with str
which is unsized, but that's OK - this function accepts a reference to T
, so nothing bad happens.
However, there is another place where question of sizedness is important. Traits in Rust are always implemented for some type:
trait A { fn do_something(&self); } struct X; impl A for X { fn do_something(&self) {} }
However, this is only necessary for convenience and practicality purposes. It is possible to define traits to always take one type parameter and to not specify the type the trait is implemented for:
// this is not actual Rust but some Rust-like language trait A<T> { fn do_something(t: &T); } struct X; impl A<X> { fn do_something(t: &X) {} }
That's how Haskell type classes work, and, in fact, that's how traits are actually implemented in Rust at a lower level.
Each trait in Rust has an implicit type parameter, called Self
, which designates the type this trait is implemented for. It is always available in the body of the trait:
trait A { fn do_something(t: &Self); }
This is where the question of sizedness comes into the picture. Is the Self
parameter sized?
It turns out that no, Self
is not sized by default in Rust. Each trait has an implicit ?Sized
bound on Self
. One of the reasons this is needed because there are a lot of traits which can be implemented for unsized types and still work. For example, any trait which only contains methods which only take and return Self
by reference can be implemented for unsized types. You can read more about motivation in RFC 546.
Sizedness is not an issue when you only define the signature of the trait and its methods. Because there is no actual code in these definitions, the compiler can't assume anything. However, when you start writing generic code which uses this trait, which includes default methods because they take an implicit Self
parameter, you should take sizedness into account. Because Self
is not sized by default, default trait methods can't return Self
by value or take it as a parameter by value. Consequently, you either need to specify that Self
must be sized by default:
trait A: Sized { ... }
or you can specify that a method can only be called if Self
is sized:
trait WithConstructor { fn new_with_param(param: usize) -> Self; fn new() -> Self where Self: Sized, { Self::new_with_param(0) } }
Let's see what would happen if you did this with an unsized type.
new()
moves the result of your new_with_param(_)
method to the caller. But unless the type is sized, how many bytes should be moved? We simply cannot know. That's why move semantics require Sized
types.
Note: The various Box
es have been designed to offer runtime services for exactly this problem.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With