Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does `Self` require a constant size at compile time for calling a free function but the equivalent trait function does not?

Tags:

rust

traits

These two functions, one trait and one free, seem to be similar but calling one of them (the trait function) is allowed while calling the other isn't:

trait A {
    fn foo(&self) {
        bar(self);    // 1. Error: `Self` does not have a constant size known at compile-time
        A::bar(self); // 2. This works
    }

    fn bar(&self) {}
}

fn bar(_a: &A) {}

(Playground link for above)

I would have thought in both cases I'm accessing via a pointer whose size is known at compile time, so what is the difference and the explanation for this behaviour ?

(Rust 1.19 stable)

like image 562
ustulation Avatar asked Sep 01 '17 09:09

ustulation


1 Answers

Because the two want totally different things.

The type of self inside of A::foo is &Self, not &A. That is, it's a pointer to the implementing type, not a pointer to a trait object. Calling A::bar is fine because it's just passing said pointer along, no extra work necessary.

Calling ::bar is a whole different cooking vessel of marine life.

The problem boils down to how the compiler represents things. Let's first consider the case where Self is a Sized type like i32. This means self is a &i32. Recall that &A is a trait object, and thus effectively has the following layout:

struct ATraitObject {
    ptr: *const (),
    vtable: *const AVtable,
}

ATraitObject.ptr needs to point to the actual value itself, and ATraitObject.vtable to the implementation of the trait for that type. The compiler can fill this out with ptr being the existing self pointer, and vtable is filled with a pointer to wherever the impl A for i32 vtable lives in memory. With that, it can call ::bar.

Now consider the case where Self is not Sized, like str. This means self is a "fat" pointer, and contains two pointer's worth of information: a pointer to the underlying data and the length of the string. When the compiler goes to create the ATraitObject, it can set ptr to self.as_ptr(), and it can set vtable... but there's nowhere for it to store the string's length!

All non-Sized types have this problem, as they all have extra information carried around in their pointers. Incidentally, this includes trait objects like &A, meaning that you also can't turn a trait object into another trait object, since you'd now need two vtables.

That's the problem: the compiler simply has no way of turning &Self where Self: !Sized into an &A. There's too much information contained in self and not enough space to store it in an &A.


To get the method to compile, add a where Self: Sized clause to the method definition. This guarantees to the compiler that the method will never be called on non-Sized types, and thus it's free to assume the conversion from &Self to &A is always possible.

like image 101
DK. Avatar answered Nov 06 '22 18:11

DK.