Why are function parameters trait bounded to Sized by default?


#1

Doesn’t that just limit the amount of things you can call it with? Or does it slow down the function if you specify an argument can be unsized? (with ?Sized)?


#2

Function arguments are passed by value unless requested otherwise, e.g.

fn take_a_thing<T>(the_t: T) { }

In this case the T must be Sized because it is going to get moved from its origin (wherever the function is being called) into the_t. The compiler needs to know how big of an object to move.

(While it’s not a direct comparison, C++ chose to be looser in this area, leading to a class of bugs called object slicing.)

If you try to do it otherwise, it doesn’t get slower – it simply won’t work:

fn take_a_thing<T: ?Sized>(the_t: T) { }

   Compiling playground v0.0.1 (file:///playground)
error[E0277]: the trait bound `T: std::marker::Sized` is not satisfied
 --> src/main.rs:1:28
  |
1 | fn take_a_thing<T: ?Sized>(the_t: T) { }
  |                            ^^^^^ the trait `std::marker::Sized` is not 
implemented for `T`
  |
  = help: consider adding a `where T: std::marker::Sized` bound
  = note: all local variables must have a statically known size

(You presumably ran into this already.)

If you want to pass something whose size is unknown, you can do it by reference. This means what you’re actually passing is not the full T, but a &T or &mut T, and the compiler knows how big those are.

fn take_a_thing<T>(the_t: &T) { }

That just borrows the T. If you actually want to transfer ownership of the T into the function, Rust’s smart-pointer types like Box and Arc are sized even if their payload types are not, so you can pass them by value in this case too.

fn take_a_thing<T>(the_t: Box<T>) { }

Hope that helps.


#3

Thanks, that makes sense, but then I don’t understand why this fails. take_list in the it_works test accepts an &L, but the compiler complains about [i32] not being sized (and changing the signature of take_list from fn take_list5<L: List<Output=i32>>(list: &L) to fn take_list5<L: List<Output=i32> + ?Sized>(list: &L) compiles fine, here is the playground).


#4

It’s still default-Sized even if you only use that parameter in unsized ways.


#5

To muse, what’s interesting is the difference with how traits default to their impl being ?Sized (this is actually to facilitate more traits being object safe). So say you have:

trait Foo {
  fn bar(self);
}

In this case, bar takes self, not &self or &mut self. If you implement this trait for, say, a struct then you end up with a move (or copy if the struct is Copy). If you implement the trait for a reference to a struct, then you get a copy of the reference (so self is really &<some_struct>).

So the type and move/copy semantics vary depending on what the trait is implemented for.

It would seem, then, that generic methods could have defaulted their type parameters to ?Sized. In the case of fn take_a_thing<T>(t: T), the t would be moved or copied depending on what was passed, including a reference, similarly to the trait function above. You wouldn’t be able to call this function with a “bare” unsized type, like str or [u8] - you’d need to pass a reference and then the T would be &str or &[u8]. The problem here would be that these “implicit” references have a lifetime yet the function signature says nothing about them. As such, the function couldn’t be type (or borrow or drop) checked on its own, which is what Rust wants to be able to do.

Once you require references to be passed explicitly to functions, this issue goes away because the compiler sees that references are at play and can type check appropriately.

Implementing a trait for a reference requires declaring lifetime parameter for it (i.e. impl<'a> Foo for &'a MyStruct) and so the compiler is (again) able to see references being in play.


#6

Right, exactly. The compiler is not doing “Sized-inference,” it’s just defaulting generic trait bounds to + Sized. This has the drawback that cases where Sized is not necessary will still get Sized, but it has the advantage that I don’t have to think very much when I hit the problem, and I’m very unlikely to introduce a change under maintenance that suddenly requires an explicit ?Sized.

The alternative of not assuming Sized would cause most code to become littered with + Sized bounds.

So I personally think the default is reasonable and in accordance with the Principle Of Least Surprise.