A different solution which avoids Deref
, Borrow
, and PhantomData
entirely:
Click to see source which stores an "unwrap" function pointer .
trait Tr {
fn use_tr(&self);
}
impl Tr for i32 {
fn use_tr(&self) {
println!("I'm integer {}.", self);
}
}
struct S<T, U> {
s: T,
unwrap: fn(&T) -> &U,
}
impl<T, U> S<T, U> {
fn new(s: T, unwrap: fn(&T) -> &U) -> Self {
Self {
s,
unwrap,
}
}
}
impl<T, U> S<T, U>
where
U: Tr,
{
fn foo(&self) {
(self.unwrap)(&self.s).use_tr();
}
}
fn main() {
let a = S::new(1i32, |x| x);
a.foo();
let b = S::new(std::rc::Rc::new(2i32), |x| &**x);
b.foo();
let c = S::new(std::sync::Arc::new(std::rc::Rc::new(2i32)), |x| &***x);
c.foo();
}
Output:
I'm integer 1.
I'm integer 2.
I'm integer 2.
(Playground)
Using a helper trait Is
, you could also provide convenience constructors new_direct
and new_indirect
:
Click to see source.
trait Is<T> {
fn id_by_ref(&self) -> &T;
}
impl<T> Is<T> for T {
fn id_by_ref(&self) -> &T {
self
}
}
impl<T, U> S<T, U> {
fn new(s: T, unwrap: fn(&T) -> &U) -> Self {
Self {
s,
unwrap,
}
}
fn new_direct(s: T) -> Self
where
T: Is<U>,
{
Self {
s,
unwrap: Is::id_by_ref,
}
}
fn new_indirect(s: T) -> Self
where
T: std::ops::Deref<Target = U>,
{
Self {
s,
unwrap: std::ops::Deref::deref,
}
}
}
fn main() {
let a = S::new_direct(1i32);
a.foo();
let b = S::new_indirect(std::rc::Rc::new(2i32));
b.foo();
}
(Playground)
I just noticed that adding an unwrap
function (instead of PhantomData
) to the struct introduces overhead, as it will store the function pointer in real memory (i.e. it's not zero cost).
Conclusion:
I think semantically, using Deref
is the right choice, because that's what you intend to do: You store some sort of pointer which can be unwrapped ("dereferenced") to a type which implements a trait (Tr
). Using the Deref::deref
method exactly provides that sort of "unwrapping", and it also stores the target type Deref::Target
for you (so you don't need turbofishes all the time later). The only problem with Deref
is that you always need to store a pointer-like type, i.e. if you don't have an Rc
but want to store the value directly, you need the dummy wrapper.
A small exercise:
"Click to see an example of how to avoid the unwrap function pointer using a zero-sized type, plus some additional remarks.
In theory (and practice), it's possible to avoid storing the function pointer by using something like a ConstFn
trait. So instead of:
One could write:
trait ConstFnByRef<Arg: ?Sized> {
type Retval: ?Sized;
const FN: fn(&Arg) -> &Self::Retval;
}
struct S<T, U> {
s: T,
_unwrap: U,
}
And then use a zero-sized type instead of a function pointer:
fn main() {
- let a = S::new(1i32, |x| x);
+ struct UnwrapNoop;
+ impl<T> ConstFnByRef<T> for UnwrapNoop {
+ type Retval = T;
+ const FN: fn(&T) -> &T = |x| x;
+ }
+ let a = S::new(1i32, UnwrapNoop);
a.foo();
(Playground)
But this gets very very verbose and likely isn't practical at all.
The method std::ops::Deref::deref
already provides the same functionality as the const FN
above, with the difference that it maps a &Self
to a &Self::Target
instead of mapping a &T
to a &Self::Retval
.
This means that using Deref
is similar to the approach with the function pointer, except that a wrapper is needed in case where there less than or more than one level of smart pointers involved.
Getting back to the OP, I would propose something like this:
struct S<P> {
p: P
}
struct Dummy<T>(pub T);
impl<T> Deref for Dummy<T> {
type Target = T;
fn deref(&self) -> &T {
&self.0
}
}
impl<T> S<Dummy<T>> {
fn new(t: T) -> Self {
Self {
p: Dummy(t),
}
}
}
impl<P> S<P> {
fn with_pointer(p: P) -> Self {
Self { p }
}
fn foo(&self)
where
P: Deref,
<P as Deref>::Target: Tr,
{
self.p.deref().use_tr()
}
}
It seems to be easy to use:
fn main() {
let a = S::new(10i32);
let b = S::with_pointer(std::rc::Rc::new(20i32));
a.foo();
b.foo();
}
(Playground)
Output:
Called method `use_tr` on integer 10.
Called method `use_tr` on integer 20.
But I'm very curious what other people think about this problem, and whether there are some downsides. (I have been struggling a lot with Deref
myself.)
I just noticed Deref
coercion also allows writing the foo
method more concise:
impl<P> S<P> {
fn with_pointer(p: P) -> Self {
Self { p }
}
fn foo(&self)
where
P: Deref,
<P as Deref>::Target: Tr,
{
- self.p.deref().use_tr()
+ self.p.use_tr()
}
}
(Playground)
That's another pro for Deref
.