Should I implement Deref in this case?

pub struct CachedVal<T> {
  pub val: T,
  pub __cache: Some_Cache
}

99% of the time, I want to use CachedVal<T> as a T -- but for one particular vertical, I cache about the __cache. I am wondering if this is a good situation to use impl Deref (as Box does) or to not to because I have a potential collision in __cache.

Also, I have skimmed Deref in std::ops - Rust and Treating Smart Pointers Like Regular References with the Deref Trait - The Rust Programming Language -- does it state what takes precedence in the case of a collision ?

If you are worried about method name collisions, you can use the same method that Box does. By name spacing the function under impl<T> CachedVal<T>, but specifying Self as an argument not named self, you can prevent the deref coercion caused by the dot operator.

Excerpt from Box::into_raw:

pub fn into_raw(b: Box<T, A>) -> *mut T

Note: this is an associated function, which means that you have to call it as Box::into_raw(b) instead of b.into_raw(). This is so that there is no conflict with a method on the inner type.

  1. The Box trick is brilliant.

  2. Can it be modified to work in my case, as __cache is a FIELD of the STRUCT, not an actual function. :frowning:

So in particular, I am worried about the case of a hypothetical CachedVal<CachedVal<T>> -- now where does the .__cache resolve to.

Deref coercion and the additional effects of the dot operator only interact with methods, not fields.

For the type CachedVal<CachedVal<T>>, this.__cache is the cache on the outer CachedVal. this would have to be manually dereferenced to access the inner cache (*this).__cache.

You shouldn't have any issues accessing __cache if you implement Deref as long as you access it as a field.

1 Like

This clarifies everything. Somehow I missed this. Thanks.

Deref does interact with field accessors though… the same way as it does with methods. Just like this.foo() only becomes desugared to (*this).foo() if this doesn’t have a foo method itself, this.bar becomes (*this).bar if this doesn’t have a bar field itself.

Additionally, it seems visibility is also taken into account.

fn type_<T>(_: &T) {
    println!("{}", std::any::type_name::<T>());
}

pub struct BazField;
    
pub struct Baz {
    field: BazField,
}

mod bar {
    pub struct BarField;

    pub struct Bar<T> {
        // private fields
        field: BarField,
        inner: T,
    }

    impl<T> Bar<T> {
        pub fn new(inner: T) -> Self {
            Self {
                field: BarField,
                inner,
            }
        }
    }

    impl<T> std::ops::Deref for Bar<T> {
        type Target = T;
        fn deref(&self) -> &T {
            &self.inner
        }
    }

    use super::*;

    pub fn from_inside(x: &Bar<Baz>) {
        type_(&x.field);
    }
}
use bar::*;

pub fn from_outside(x: &Bar<Baz>) {
    type_(&x.field);
}

fn main() {
    let x = Bar::new(Baz { field: BazField });
    from_inside(&x);
    from_outside(&x);
}
playground::bar::BarField
playground::BazField

This means the Box trick can still fully apply if you make your field private and only accessible via some method CachedVal::cache like fn cache(val: &Self) -> &Some_Cache (and perhaps a cache_mut equivalent if mutable access is needed, too).

2 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.