Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why can a function on a trait object not be called when bounded with `Self: Sized`?

I have the following code:

trait Bar {
    fn baz(&self, arg: impl AsRef<str>)
    where
        Self: Sized;
}

struct Foo;

impl Bar for Foo {
    fn baz(&self, arg: impl AsRef<str>) {}
}

fn main() {
    let boxed: Box<dyn Bar> = Box::new(Foo);
    boxed.baz();
}

playground

Which results in this error:

error: the `baz` method cannot be invoked on a trait object
  --> src/main.rs:15:11
   |
15 |     boxed.baz();
   |           ^^^

Why is this not possible? It works when I remove the Self: Sized bound, but then I can't use generics which make the function more comfortable for the caller.

This is not a duplicate of Why does a generic method inside a trait require trait object to be sized? which asks why you can't call baz from a trait object. I'm not asking why the bound is required; this has already been discussed.

like image 757
Tim Diekmann Avatar asked Aug 13 '18 12:08

Tim Diekmann


People also ask

What is sized trait rust?

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.

What is a trait object in Rust?

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 object safe?

Object-safe traits are traits with methods that follow these two rules: the return type is not Self. there are no generic types parameters.

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.


2 Answers

Because Rust's generics system works through monomorphization.

In Java, for example, type parameters in a generic function turn into variables of type Object, and are casted as necessary. Generics in languages like this simply serves as a tool to help verify the correctness of types within code.

Languages such as Rust and C++ use monomorphization for generics. For each combination of type parameters a generic function is invoked with, specialized machine code is generated which runs that function with those combinations of type parameters. The function is monomorphized. This allows data to be stored in place, eliminates the cost of casting, and allows the generic code to call "static" functions on that type paramameter.

So why can't you do that on a trait object?

Trait objects in many languages, including Rust, are implemented using a vtable. When you have some type of pointer to a trait object (raw, reference, Box, reference counter, etc.), it contains two pointers: the pointer to the data, and a pointer to a vtable entry. The vtable entry is a collection of function pointers, stored in an immutable memory region, which point to the implementation of that trait's methods. Thus, when you call a method on a trait object, it looks up the function pointer of the implementation in the vtable, and then makes an indirect jump to that pointer.

Unfortunately, the Rust compiler cannot monomorphize functions, if it does not know at compile time the code that implements the function, which is the case when you call a method on a trait object. For that reason, you cannot call a generic function (well, generic over types) on a trait object.

-Edit-

It sounds like you're asking why the : Sized restriction is necessary.

: Sized makes it so that the trait cannot be used as a trait object. I suppose there could be a couple of alternatives. Rust could implicitly make any trait with generic functions not object safe. Rust could also implicitly prevent generic functions from being called on trait objects.

However, Rust tries to be explicit with what the compiler is doing, which these implicit approaches would go against. Wouldn't it be confusing, anyways, for a beginner to try and call a generic function on a trait object and have it fail to compile?

Instead, Rust lets you explicitly make the entire trait not object safe

trait Foo: Sized {

Or explicitly make certain functions only available with static dispatch

fn foo<T>() where Self: Sized {

like image 197
Phoenix Avatar answered Oct 21 '22 18:10

Phoenix


The bound makes the method not object safe. Traits that are not object safe cannot be used as types.

Methods that take Self as an argument, return Self or otherwise require Self: Sized are not Object safe. That's because methods on a trait object are called via dynamic dispatch and the size of the trait implementation cannot be known at compile time. -- Peter Hall

Citing the official docs:

Only traits that are object-safe can be made into trait objects. A trait is object-safe if both of these are true:

  • the trait does not require that Self: Sized
  • all of its methods are object-safe

So what makes a method object-safe? Each method must require that Self: Sized or all of the following:

  • must not have any type parameters
  • must not use Self

See also:

  • this answer from Why does a generic method inside a trait require trait object to be sized?
like image 33
Tim Diekmann Avatar answered Oct 21 '22 19:10

Tim Diekmann