Downcasting Self
parameters
Now let's move on to something a little more complicated. We
mentioned before that
Self
is not accepted outside of the receiver, such as when it's
another parameter, as there is no guarantee that the other
parameter has the same base type as the receiver (and if they
are not the same base type, there is no actual implementation to
call).
Let's see how we can work around this to implement
PartialOrd
for dyn Trait
, despite the &Self
parameter. The trait is a good
fit in the face of type erasure, as we can just return None
when
the types don't match, indicating that comparison is not possible.
PartialOrd
requires PartialEq
, so we'll tackle that as well.
Downcasting with dyn Any
to emulate dynamic typing
We haven't had to use dyn Any
in the previous examples, because
we've been able to maneuver our implementations in such a way that
dynamic dispatch implicitly "downcasted" our erased types to their
concrete base types for us. It's able to do this because the pointer
to the base type is coupled with a vtable that only accepts said base
type, and there is no need for actual dynamic typing or comparing types
at runtime. The conversion is infallible for those cases.
However, now we have two wide pointers which may point to different base types. In this particular application, we only really need to know if they have the same base type or not... though it would be nice to have some safe way to recover the erased type of non-receiver too, instead of whatever casting shenanigans might be necessary.
You might think you could somehow use the vtable pointers to see if the base types are the same. But unfortunately, we can't rely on the vtable to compare their types at runtime.
When comparing wide pointers, both the address and the metadata are tested for equality. However, note that comparing trait object pointers (
*const dyn Trait
) is unreliable: pointers to values of the same underlying type can compare unequal (because vtables are duplicated in multiple codegen units), and pointers to values of different underlying type can compare equal (since identical vtables can be deduplicated within a codegen unit).
That's right, false negatives and false positives. Fun!
So we need a different mechanism to compare types and know when we
have two wide pointers to the same base type, and that's where dyn Any
comes in. Any
is
the trait to emulate dynamic typing, and
many fallible downcasting methods
are supplied for the type-erased forms of dyn Any
, Box<dyn Any + Send>
,
et cetera. This will allow us to not just compare for base type equality,
but also to safely recover the erased base type ("downcast").
The Any
trait comes with a 'static
constraint for soundness reasons,
so note that our base types are going to be more limited for this example.
Additionally, the lack of supertrait upcasting is going to make things less ergonomic than they will be once that feature is available.
One last side note, we look at dyn Any
in a bit more detail later.
Well enough meta, let's dive in!
PartialEq
The general idea is that we're going to have a comparison trait, DynCompare
,
and then implement PartialEq
for dyn DynCompare
in a universal manner.
Then our actual trait (Trait
) can have DynCompare
as a supertrait, and
implement PartialEq
for dyn Trait
by upcasting to dyn DynCompare
.
In the implementation for dyn DynCompare
, we're going to have to (attempt to)
downcast to the erased base type. For that to be available we will need to
first be able to upcast from dyn DynCompare
to dyn Any
.
As the first step, we're going to use the "supertrait we can blanket implement" pattern yet again to make a trait that can handle all of our supertrait upcasting needs.
Here it is, similar to how we've done it before:
#![allow(unused)] fn main() { use std::any::Any; trait AsDynCompare: Any { fn as_any(&self) -> &dyn Any; fn as_dyn_compare(&self) -> &dyn DynCompare; } // Sized types only impl<T: Any + DynCompare> AsDynCompare for T { fn as_any(&self) -> &dyn Any { self } fn as_dyn_compare(&self) -> &dyn DynCompare { self } } trait DynCompare: AsDynCompare { fn dyn_eq(&self, other: &dyn DynCompare) -> bool; } }
There's an Any: 'static
bound which applies to dyn Any + '_
, so
all of those &dyn Any
are actually &dyn Any + 'static
.
I have also included an Any
supertrait to AsDynCompare
, so the
"always 'static
" property holds for &dyn DynCompare
as well, even
though it isn't strictly necessary. This way, we don't have to worry
about being flexible with the trait object lifetime at all -- it is
just always 'static
.
The downside is that only base types that satisfy the 'static
bound
can be supported, so there may be niche circumstances where you don't
want to include the supertrait bound. However, given that we need to
upcast to dyn Any
, this must mean you're pretending to be another
type, which seems quite niche indeed. If you do try the non-'static
route for your own use case, note that some of the implementations in
this example could be made more general.
Anyway, let's move on to performing cross-type equality checking:
#![allow(unused)] fn main() { use std::any::Any; trait AsDynCompare: Any { fn as_any(&self) -> &dyn Any; fn as_dyn_compare(&self) -> &dyn DynCompare; } impl<T: Any + DynCompare> AsDynCompare for T { fn as_any(&self) -> &dyn Any { self } fn as_dyn_compare(&self) -> &dyn DynCompare { self } } trait DynCompare: AsDynCompare { fn dyn_eq(&self, other: &dyn DynCompare) -> bool; } impl<T: Any + PartialEq> DynCompare for T { fn dyn_eq(&self, other: &dyn DynCompare) -> bool { if let Some(other) = other.as_any().downcast_ref::<Self>() { self == other } else { false } } } // n.b. this could be implemented in a more general way when // the trait object lifetime is not constrained to `'static` impl PartialEq<dyn DynCompare> for dyn DynCompare { fn eq(&self, other: &dyn DynCompare) -> bool { self.dyn_eq(other) } } }
Here we've utilized our dyn Any
upcasting to try and recover a
parameter of our own base type, and if successful, do the actual
(partial) comparison. Otherwise we say they're not equal.
This allows us to implement PartialEq
for dyn Compare
.
Then we want to wire this functionality up to our actual trait:
#![allow(unused)] fn main() { use std::any::Any; trait AsDynCompare: Any { fn as_any(&self) -> &dyn Any; fn as_dyn_compare(&self) -> &dyn DynCompare; } impl<T: Any + DynCompare> AsDynCompare for T { fn as_any(&self) -> &dyn Any { self } fn as_dyn_compare(&self) -> &dyn DynCompare { self } } trait DynCompare: AsDynCompare { fn dyn_eq(&self, other: &dyn DynCompare) -> bool; } impl<T: Any + PartialEq> DynCompare for T { fn dyn_eq(&self, other: &dyn DynCompare) -> bool { if let Some(other) = other.as_any().downcast_ref::<Self>() { self == other } else { false } } } impl PartialEq<dyn DynCompare> for dyn DynCompare { fn eq(&self, other: &dyn DynCompare) -> bool { self.dyn_eq(other) } } trait Trait: DynCompare {} impl Trait for i32 {} impl Trait for bool {} impl PartialEq<dyn Trait> for dyn Trait { fn eq(&self, other: &dyn Trait) -> bool { self.as_dyn_compare() == other.as_dyn_compare() } } }
The supertrait bound does most of the work, and we just use
upcasting again -- to dyn DynCompare
this time -- to be
able to perform PartialEq
on our dyn Trait
.
A blanket implementation in std
gives us PartialEq
for Box<dyn Trait>
automatically.
Now let's try it out:
use std::any::Any; trait AsDynCompare: Any { fn as_any(&self) -> &dyn Any; fn as_dyn_compare(&self) -> &dyn DynCompare; } impl<T: Any + DynCompare> AsDynCompare for T { fn as_any(&self) -> &dyn Any { self } fn as_dyn_compare(&self) -> &dyn DynCompare { self } } trait DynCompare: AsDynCompare { fn dyn_eq(&self, other: &dyn DynCompare) -> bool; } impl<T: Any + PartialEq> DynCompare for T { fn dyn_eq(&self, other: &dyn DynCompare) -> bool { if let Some(other) = other.as_any().downcast_ref::<Self>() { self == other } else { false } } } impl PartialEq<dyn DynCompare> for dyn DynCompare { fn eq(&self, other: &dyn DynCompare) -> bool { self.dyn_eq(other) } } trait Trait: DynCompare {} impl Trait for i32 {} impl Trait for bool {} impl PartialEq<dyn Trait> for dyn Trait { fn eq(&self, other: &dyn Trait) -> bool { self.as_dyn_compare() == other.as_dyn_compare() } } fn main() { let bx1a: Box<dyn Trait> = Box::new(1); let bx1b: Box<dyn Trait> = Box::new(1); let bx2: Box<dyn Trait> = Box::new(2); let bx3: Box<dyn Trait> = Box::new(true); println!("{}", bx1a == bx1a); println!("{}", bx1a == bx1b); println!("{}", bx1a == bx2); println!("{}", bx1a == bx3); }
Uh... it didn't work, but for weird reasons. Why is it trying to move out of the
Box
for a comparison? As it turns out, this is a longstanding bug in the
language. Fortunately that issue
also offers a workaround that's ergonomic at the use site: implement PartialEq<&Self>
too.
use std::any::Any; trait AsDynCompare: Any { fn as_any(&self) -> &dyn Any; fn as_dyn_compare(&self) -> &dyn DynCompare; } // Sized types only impl<T: Any + DynCompare> AsDynCompare for T { fn as_any(&self) -> &dyn Any { self } fn as_dyn_compare(&self) -> &dyn DynCompare { self } } trait DynCompare: AsDynCompare { fn dyn_eq(&self, other: &dyn DynCompare) -> bool; } impl<T: Any + PartialEq> DynCompare for T { fn dyn_eq(&self, other: &dyn DynCompare) -> bool { if let Some(other) = other.as_any().downcast_ref::<Self>() { self == other } else { false } } } impl PartialEq<dyn DynCompare> for dyn DynCompare { fn eq(&self, other: &dyn DynCompare) -> bool { self.dyn_eq(other) } } trait Trait: DynCompare {} impl Trait for i32 {} impl Trait for bool {} impl PartialEq<dyn Trait> for dyn Trait { fn eq(&self, other: &dyn Trait) -> bool { self.as_dyn_compare() == other.as_dyn_compare() } } // New impl PartialEq<&Self> for Box<dyn Trait> { fn eq(&self, other: &&Self) -> bool { <Self as PartialEq>::eq(self, *other) } } fn main() { let bx1a: Box<dyn Trait> = Box::new(1); let bx1b: Box<dyn Trait> = Box::new(1); let bx2: Box<dyn Trait> = Box::new(2); let bx3: Box<dyn Trait> = Box::new(true); println!("{}", bx1a == bx1a); println!("{}", bx1a == bx1b); println!("{}", bx1a == bx2); println!("{}", bx1a == bx3); }
Ok, now it works. Phew!
PartialOrd
From here it's mostly mechanical to add PartialOrd
support:
+use core::cmp::Ordering;
trait DynCompare: AsDynCompare {
fn dyn_eq(&self, other: &dyn DynCompare) -> bool;
+ fn dyn_partial_cmp(&self, other: &dyn DynCompare) -> Option<Ordering>;
}
-impl<T: Any + PartialEq> DynCompare for T {
+impl<T: Any + PartialOrd> DynCompare for T {
fn dyn_eq(&self, other: &dyn DynCompare) -> bool {
if let Some(other) = other.as_any().downcast_ref::<Self>() {
self == other
} else {
false
}
}
+
+ fn dyn_partial_cmp(&self, other: &dyn DynCompare) -> Option<Ordering> {
+ other
+ .as_any()
+ .downcast_ref::<Self>()
+ .and_then(|other| self.partial_cmp(other))
+ }
}
+impl PartialOrd<dyn DynCompare> for dyn DynCompare {
+ fn partial_cmp(&self, other: &dyn DynCompare) -> Option<Ordering> {
+ self.dyn_partial_cmp(other)
+ }
+}
+impl PartialOrd<dyn Trait> for dyn Trait {
+ fn partial_cmp(&self, other: &dyn Trait) -> Option<Ordering> {
+ self.as_dyn_compare().partial_cmp(other.as_dyn_compare())
+ }
+}
+impl PartialOrd<&Self> for Box<dyn Trait> {
+ fn partial_cmp(&self, other: &&Self) -> Option<Ordering> {
+ <Self as PartialOrd>::partial_cmp(self, *other)
+ }
+}