Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do you actually use dynamically sized types in Rust?

Tags:

rust

In theory, Dynamically-Sized Types (DST) have landed and we should now be able to use dynamically sized type instances. Practically speaking, I can neither make it work, nor understand the tests around it.

Everything seems to revolve around the Sized? keyword... but how exactly do you use it?

I can put some types together:

// Note that this code example predates Rust 1.0
// and is no longer syntactically valid

trait Foo for Sized? {
    fn foo(&self) -> u32;
}

struct Bar;
struct Bar2;

impl Foo for Bar { fn foo(&self) -> u32 { return 9u32; }}
impl Foo for Bar2 { fn foo(&self) -> u32 { return 10u32; }}

struct HasFoo<Sized? X> {
    pub f:X
}

...but how do I create an instance of HasFoo, which is DST, to have either a Bar or Bar2?

Attempting to do so always seems to result in:

<anon>:28:17: 30:4 error: trying to initialise a dynamically sized struct
<anon>:28   let has_foo = &HasFoo {

I understand broadly speaking that you can't have a bare dynamically sized type; you can only interface with one through a pointer, but I can't figure out how to do that.

like image 765
Doug Avatar asked Sep 09 '14 09:09

Doug


People also ask

What is dyn Rust?

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.

What is sized in Rust?

In Rust a type is sized if its size in bytes can be determined at compile-time. Determining a type's size is important for being able to allocate enough space for instances of that type on the stack. Sized types can be passed around by value or by reference.

What is a trait object?

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.

What is a type in Rust?

Every variable, item, and value in a Rust program has a type. The type of a value defines the interpretation of the memory holding it and the operations that may be performed on the value. Built-in types are tightly integrated into the language, in nontrivial ways that are not possible to emulate in user-defined types.


2 Answers

Disclaimer: these are just the results of a few experiments I did, combined with reading Niko Matsakis's blog.

DSTs are types where the size is not necessarily known at compile time.

Before DSTs

A slice like [i32] or a bare trait like IntoIterator were not valid object types because they do not have a known size.

A struct could look like this:

// [i32; 2] is a fixed-sized vector with 2 i32 elements
struct Foo {
    f: [i32; 2],
}

or like this:

// & is basically a pointer.
// The compiler always knows the size of a
// pointer on a specific architecture, so whatever
// size the [i32] has, its address (the pointer) is
// a statically-sized type too
struct Foo2<'a> {
    f: &'a [i32],
}

but not like this:

// f is (statically) unsized, so Foo is unsized too
struct Foo {
    f: [i32],
}

This was true for enums and tuples too.

With DSTs

You can declare a struct (or enum or tuple) like Foo above, containing an unsized type. A type containing an unsized type will be unsized too.

While defining Foo was easy, creating an instance of Foo is still hard and subject to change. Since you can't technically create an unsized type by definition, you have to create a sized counterpart of Foo. For example, Foo { f: [1, 2, 3] }, a Foo<[i32; 3]>, which has a statically known size and code some plumbing to let the compiler know how it can coerce this into its statically unsized counterpart Foo<[i32]>. The way to do this in safe and stable Rust is still being worked on as of Rust 1.5 (here is the RFC for DST coercions for more info).

Luckily, defining a new DST is not something you will be likely to do, unless you are creating a new type of smart pointer (like Rc), which should be a rare enough occurrence.

Imagine Rc is defined like our Foo above. Since it has all the plumbing to do the coercion from sized to unsized, it can be used to do this:

use std::rc::Rc;

trait Foo {
    fn foo(&self) {
        println!("foo")
    }
}
struct Bar;

impl Foo for Bar {}

fn main() {
    let data: Rc<Foo> = Rc::new(Bar);
    // we're creating a statically typed version of Bar
    // and coercing it (the :Rc<Foo> on the left-end side)
    // to as unsized bare trait counterpart.
    // Rc<Foo> is a trait object, so it has no statically
    // known size
    data.foo();
}

playground example

?Sized bound

Since you're unlikely to create a new DST, what are DSTs useful for in your everyday Rust coding? Most frequently, they let you write generic code that works both on sized types and on their existing unsized counterparts. Most often these will be Vec/[] slices or String/str.

The way you express this is through the ?Sized "bound". ?Sized is in some ways the opposite of a bound; it actually says that T can be either sized or unsized, so it widens the possible types we can use, instead of restricting them the way bounds typically do.

Contrived example time! Let's say that we have a FooSized struct that just wraps a reference and a simple Print trait that we want to implement for it.

struct FooSized<'a, T>(&'a T)
where
    T: 'a;

trait Print {
    fn print(&self);
}

We want to define a blanket impl for all the wrapped T's that implement Display.

impl<'a, T> Print for FooSized<'a, T>
where
    T: 'a + fmt::Display,
{
    fn print(&self) {
        println!("{}", self.0)
    }
}

Let's try to make it work:

// Does not compile. "hello" is a &'static str, so self print is str
// (which is not sized)
let h_s = FooSized("hello");
h_s.print();

// to make it work we need a &&str or a &String
let s = "hello"; // &'static str
let h_s = &s; // & &str
h_s.print(); // now self is a &str

Eh... this is awkward... Luckily we have a way to generalize the struct to work directly with str (and unsized types in general): ?Sized

//same as before, only added the ?Sized bound
struct Foo<'a, T: ?Sized>(&'a T)
where
    T: 'a;

impl<'a, T: ?Sized> Print for Foo<'a, T>
where
    T: 'a + fmt::Display,
{
    fn print(&self) {
        println!("{}", self.0)
    }
}

now this works:

let h = Foo("hello");
h.print();

playground

For a less contrived (but simple) actual example, you can look at the Borrow trait in the standard library.

Back to your question

trait Foo for ?Sized {
    fn foo(&self) -> i32;
}

the for ?Sized syntax is now obsolete. It used to refer to the type of Self, declaring that `Foo can be implemented by an unsized type, but this is now the default. Any trait can now be implemented for an unsized type, i.e. you can now have:

trait Foo {
    fn foo(&self) -> i32;
}

//[i32] is unsized, but the compiler does not complain for this impl
impl Foo for [i32] {
    fn foo(&self) -> i32 {
        5
    }
}

If you don't want your trait to be implementable for unsized types, you can use the Sized bound:

// now the impl Foo for [i32] is illegal
trait Foo: Sized {
    fn foo(&self) -> i32;
}
like image 50
Paolo Falabella Avatar answered Sep 28 '22 04:09

Paolo Falabella


To amend the example that Paolo Falabella has given, here is a different way of looking at it with the use of a property.

struct Foo<'a, T>
where
    T: 'a + ?Sized,
{
    printable_object: &'a T,
}

impl<'a, T> Print for Foo<'a, T>
where
    T: 'a + ?Sized + fmt::Display,
{
    fn print(&self) {
        println!("{}", self.printable_object);
    }
}

fn main() {
    let h = Foo {
        printable_object: "hello",
    };
    h.print();
}
like image 36
nate Avatar answered Sep 28 '22 03:09

nate