3

I'm having trouble understanding traits and object safety in Rust.

I have a StoreTrait for storing some data and a Resource struct that holds a reference to a StoreTrait.

I want the Resource to have a reference to a store intance, because many of the methods of Resource will use store, and I don't want to explicitly pass store to every method on Resource.

I also need to have the logic reside in the trait, because I have various impls that will need to share it (an in-memory and an on-disk store). So moving it into the impl is not what I'd prefer.

In the Store trait, I try passing &Self to a function, but it fails because &Self is not Sized:

pub trait StoreTrait {
    fn create_resource(&self) {
        let agent = Resource::new(self);
    }
}

struct Resource<'a> {
    store: &'a dyn StoreTrait,
}

impl<'a> Resource<'a> {
    pub fn new(store: &dyn StoreTrait) -> Resource {
        Resource { store }
    }
}
error[E0277]: the size for values of type `Self` cannot be known at compilation time
 --> src/lib.rs:3:35
  |
3 |         let agent = Resource::new(self);
  |                                   ^^^^ doesn't have a size known at compile-time
  |
  = note: required for the cast to the object type `dyn StoreTrait`
help: consider further restricting `Self`
  |
2 |     fn create_resource(&self) where Self: Sized {
  |                               ^^^^^^^^^^^^^^^^^

This is where this might become an XY problem

The compiler suggests using where Self: Sized bounds in these methods. However, this causes another problem later when calling save_resource() from a Resource, since that means I'm invoking a method on a trait object with a Sized bound.

pub trait StoreTrait {
    // So after adding the trait bounds here...
    fn create_resource(&self)
    where
        Self: Sized,
    {
        let agent = Resource::new(self);
    }

    // And here (internal logic requires that)...
    fn save_resource(&self, resource: Resource)
    where
        Self: Sized,
    {
        // This now requires `Self: Sized`, too!
        self.create_resource()
    }
}

pub struct Resource<'a> {
    pub store: &'a dyn StoreTrait,
}

impl<'a> Resource<'a> {
    pub fn new(store: &dyn StoreTrait) -> Resource {
        Resource { store }
    }

    pub fn save(&self) {
        self.store.save_resource(self)
    }
}

playground

error: the `save_resource` method cannot be invoked on a trait object
  --> src/lib.rs:26:20
   |
13 |         Self: Sized;
   |               ----- this has a `Sized` requirement
...
26 |         self.store.save_resource(self)
   |                    ^^^^^^^^^^^^^

How do I circumvent setting the trait bound? Or how do I prevent calling a method on a trait object? Perhaps I'm doing something else that doesn't make a ton of sense?

edit: I ended up changing the arguments for the functions. Whenever I used &dyn StoreTrait, I switched to &impl StoreTrait. This means the functions with that signature are compiled for every implementation, which makes the binary a bit bigger, but it now works with the sized requirement. yay!

joepio
  • 4,266
  • 2
  • 17
  • 19
  • Why does your internal logic require `Self: Sized` on `save_resource`? Why would you possibly want to _prevent_ methods being called on trait objects, when you at the same time _want_ to call a method on trait object (`self.store` is a trait object, and you call `save_resource()` on it). – user4815162342 Nov 23 '20 at 20:05
  • Your first issue can be fixed by replacing a default implementation with a [blanket implementation](https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=243c0b12ac721ccf15bf4d40c40e8a03). Does that help? – user4815162342 Nov 23 '20 at 20:06
  • @user4815162342 Thanks for the reply! The `Self: Sized` is required because it calls `self.create_resource()`, which in turn requires sized. I'll update the question. The blanket implementation won't work, because various implementations of the Trait must be possible that have different logic. – joepio Nov 23 '20 at 20:18
  • Ok, but `create_resource()` doesn't require `Sized` in the modified code I've shown. Does that solve the problem? – user4815162342 Nov 23 '20 at 20:20
  • @user4815162342 unfortunately, that does not work in my case, because I have separate `impl`s. I have an on-disk store and an in-memory store that [share a lot of logic](https://github.com/joepio/atomic/blob/master/lib/src/storelike.rs). I could move all the logic to the impls, but that would defeat the purpose of using a trait. – joepio Nov 23 '20 at 20:28
  • 1
    *that share a lot of logic* — create a new type that holds the shared logic and is parameterized by one (or more) generic pieces that specify the unique behavior. Composition over inheritance. – Shepmaster Nov 23 '20 at 20:33
  • @Shepmaster Thanks for the help, but I don't think I'm understanding this correctly. I have one trait and two impls, but should I have more impls that are more generic? – joepio Nov 23 '20 at 20:42
  • 1
    Maybe it would help if you trimmed your code to a minimal example that still shows the issue - two separate impls and all that, but with unnecessary stuff removed (like actually working with the files). Otherwise we risk solving the various "ys" of an [xy problem](https://xyproblem.info/) (such as how I eliminated your `Self: Sized` restriction) without getting to the bottom of it. – user4815162342 Nov 23 '20 at 20:48
  • [Sure, why not](https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=89e8f5e2a3a02d3877f2f448a5fd0695)? – Shepmaster Nov 23 '20 at 20:49
  • @user4815162342 You're right, sorry. I've updated the [playground](https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=f91d0ad0a726776eca188f746aa0a985) to include two impls. – joepio Nov 23 '20 at 20:55
  • "I want the Resource to have a reference to a store intance" - This probably *will* come back to bite you in the future. Also, are resources really tied to the store they were loaded from? What if you want to load it from one store, then save it to a different one? Is that a valid scenario? What, then, does it mean for the resource to reference "the" store? – Sebastian Redl Nov 24 '20 at 09:24
  • @SebastianRedl The Resource struct in the project is designed to provide a more convenient way to manipulate individual Resources in a particular Store. Not having a reference inside of the Resource will mean that for many methods on the Resource, the store has to be passed explicitly, which results in a clunky API. It's certainly preferable to code that won't compile, but I really hoped that it would be possible to solve it while keeping the friendly API. – joepio Nov 24 '20 at 16:59

1 Answers1

0

Perhaps if you just move function from the trait to each implementation it will do what you want?

fn main() {}

pub trait StoreTrait {
    fn create_resource(&self);

    fn save_resource(&self, resource: &Resource);
}

struct Resource<'a> {
    store: &'a dyn StoreTrait,
}

impl<'a> Resource<'a> {
    pub fn new(store: &dyn StoreTrait) -> Resource {
        Resource { store }
    }

    pub fn edit(&self) {
        self.store.save_resource(self)
    }
}

struct StoreMem {
    resources: Vec<String>,
}

impl StoreTrait for StoreMem {
    fn create_resource(&self) {
        let agent = Resource::new(self);
    }
    
    fn save_resource(&self, resource: &Resource) {
        //
    }
}

struct StoreDisk {
    resources: Vec<String>,
}

impl StoreTrait for StoreDisk {
    fn create_resource(&self) {
        let agent = Resource::new(self);
    }
    
    fn save_resource(&self, resource: &Resource) {
        //
    }
}
Bob Bobbio
  • 577
  • 2
  • 5
  • 10
  • That does compile (because the structs have a known size, contrary to the trait), but it defeats the purpose of using a trait: to share logic between two structs. – joepio Nov 24 '20 at 16:46