Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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, we’ll rely on the relatively new ability of supertrait upcasting to make things more ergonomic. If your MSRV doesn’t allow for that, the example is still possible, but you’ll need to supply your own upcasting ability. The bottom of this chapter shows what that would look like.

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.

Before trait upcasting, we would use the “supertrait we can blanket implement” pattern yet again. But with modern Rust, we only need the supertrait bound on DynCompare.

#![allow(unused)]
fn main() {
use std::any::Any;

trait DynCompare: Any {
    fn dyn_eq(&self, other: &dyn DynCompare) -> bool;
}
}

There’s an Any: 'static bound which applies to dyn Any + '_, so &dyn Any is always actually &(dyn Any + 'static). And due to the Any supertrait on AsDynCompare, the “always 'static” property holds for &dyn DynCompare as well. An upside of this is that we don’t have to worry about being flexible with the dyn lifetime at all – it is 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; you will also need some other approach to support upcasting.

Anyway, let’s move on to performing cross-type equality checking:

#![allow(unused)]
fn main() {
use std::any::Any;
trait DynCompare: Any {
   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 &dyn 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 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 DynCompare: Any {
   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 &dyn 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 DynCompare) == (other as &dyn DynCompare)
    }
}
}

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 DynCompare: Any {
   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 &dyn 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 DynCompare) == (other as &dyn DynCompare)
    }
}
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 DynCompare: Any {
   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 &dyn 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 DynCompare) == (other as &dyn DynCompare)
    }
}
// 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: Any {
     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 &dyn Any).downcast_ref::<Self>() {
             self == other
         } else {
             false
         }
     }
+
+    fn dyn_partial_cmp(&self, other: &dyn DynCompare) -> Option<Ordering> {
+        (other as &dyn 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 DynCompare).partial_cmp(other as &dyn DynCompare)
+    }
+}

+impl PartialOrd<&Self> for Box<dyn Trait> {
+    fn partial_cmp(&self, other: &&Self) -> Option<Ordering> {
+        <Self as PartialOrd>::partial_cmp(self, *other)
+    }
+}

Here’s the final playground.

Comparison with manual supertrait upcasting

Here’s a playground which uses manual supertrait upcasting instead.

There is more boilerplate, but arguably the method calls are more ergonomic:

impl PartialEq<dyn Trait> for dyn Trait {
    fn eq(&self, other: &dyn Trait) -> bool {
        // (self as &dyn DynCompare) == (other as &dyn DynCompare)
        self.as_dyn_compare() == other.as_dyn_compare()
    }
}

impl<T: Any + PartialOrd> DynCompare for T {
    fn dyn_eq(&self, other: &dyn DynCompare) -> bool {
        // (other as &dyn Any).downcast_ref::<Self>()
        if let Some(other) = other.as_any().downcast_ref::<Self>() {

Depending on how your traits are used, it may or may not be worth it to provide the AsDynCompare methods on top of built-in trait upcasting. It need not be a supertrait anymore, though, and can be done independently of the built-in trait upcasting implementation explored above.