Return value, ?Sized, compile time

Consider the following code (refactored from code I am trying to read/modify)

EDIT: renamed some vars to make things clearer


pub struct Foo {}

pub trait GetStuff<Target> {
    type Elem: ?Sized;
    fn get1(&self) -> Self::Elem;}

impl GetStuff<i32> for Foo {
    type Elem = i32;

    fn get1(&self) -> Self::Elem {
        todo!()}}

impl GetStuff<i64> for Foo {
    type Elem = i64;

    fn get1(&self) -> Self::Elem {
        todo!()}}

impl Foo {
    pub fn get2<T, V: GetStuff<T>>(&self) -> V::Elem {
        todo!()}}


This gets a compiler error of:

   = help: the trait `Sized` is not implemented for `<V as GetStuff<T>>::Elem`
   = note: the return type of a function must have a statically known size
help: consider further restricting the associated type

I understand that V::Elem is ?Sized

What I don't understand is: why is it okay as a return value in the two impl traits, but not okay in the last impl ?

In the first impls, the compiler knows the concrete type of Elem, and that it's Sized.

The third case, however, is generic: you can provide an impl that is unsized.

2 Likes

Sorry, how do I fix this?

Either make V::Elem: Sized or return a pointer to it, like &V::Elem or Box<V::Elem>.

3 Likes

There's no other way around this?

Not really I don't think. You can't return an unsized item from a function. Is there a reason V::Elem needs to be ?Sized?

2 Likes

I'm not sure; maybe I screwed up refactoring this. (Trying to study some new Trait based techniques).

1 Like

Yep, I've been there. :slight_smile: It's like big puzzle that you're not sure whether or not will finally snap together when you're done.

1 Like

Yeah, I misread something. The 'correct' answer is to have get return &V::Elem as @chrefr suggested.

1 Like

The problem is not multiple traits, the problem is that parameters and return values must be sized. Since you've explicitly said that the type parameter permits unsized types, it can't be used in either argument position or in return position directly.

The reason for this constraint is that Rust is designed to compile to native code fairly directly. Calling conventions for passing a value to a function need to do things like allocate space on the stack for the value or assign the value to a register large enough to hold it, using code generated when the specific values are not known - only their type is. If the type has no fixed size, then it's not possible to generate code that does that kind of allocation.

There are ways to indirect access to unsized values, and the compiler could, in theory, insert those indirections for you, but that runs up against Rust's "zero cost abstractions" philosophy - it would create additional overhead per call/per return site, so Rust expects you to ask for that explicitly by making those indirections clear in your code.

You can use unsized types as parameters to other types, and they can be manipulated through borrowed references (which are sized), boxes (ditto), reference-counted shared pointers (tritto), and so on. Which is appropriate for you depends on the use case, but Box<V::Elem> is usually a reasonable starting point if you're returning a newly-minted value, or &V::Elem if you're returning one that existed before the function was called.