20

Consider the following toy example:

use std::cmp::Ordering;

pub trait SimpleOrder {
    fn key(&self) -> u32;
}

impl PartialOrd for dyn SimpleOrder {
    fn partial_cmp(&self, other: &dyn SimpleOrder) -> Option<Ordering> {
        Some(self.cmp(other))
    }
}

impl Ord for dyn SimpleOrder {
    fn cmp(&self, other: &dyn SimpleOrder) -> Ordering {
        self.key().cmp(&other.key())
    }
}

impl PartialEq for dyn SimpleOrder {
    fn eq(&self, other: &dyn SimpleOrder) -> bool {
        self.key() == other.key()
    }
}

impl Eq for SimpleOrder {}

This doesn't compile. It claims there is a lifetime issue in the implementation for partial_cmp:

error[E0495]: cannot infer an appropriate lifetime due to conflicting requirements
 --> src/main.rs:9:23
  |
9 |         Some(self.cmp(other))
  |                       ^^^^^
  |
note: first, the lifetime cannot outlive the anonymous lifetime #2 defined on the method body at 8:5...
 --> src/main.rs:8:5
  |
8 | /     fn partial_cmp(&self, other: &dyn SimpleOrder) -> Option<Ordering> {
9 | |         Some(self.cmp(other))
10| |     }
  | |_____^
note: ...so that the declared lifetime parameter bounds are satisfied
 --> src/main.rs:9:23
  |
9 |         Some(self.cmp(other))
  |                       ^^^^^
  = note: but, the lifetime must be valid for the static lifetime...
  = note: ...so that the types are compatible:
          expected std::cmp::Eq
             found std::cmp::Eq

I really don't understand this error. In particular "expected std::cmp::Eq found std::cmp::Eq" is puzzling.

If I inline the call manually it compiles fine:

fn partial_cmp(&self, other: &dyn SimpleOrder) -> Option<Ordering> {
    Some(self.key().cmp(&other.key()))
}

What's going on here?

Peter Hall
  • 53,120
  • 14
  • 139
  • 204
orlp
  • 112,504
  • 36
  • 218
  • 315
  • 2
    This **is** mysterious! – Peter Hall Jan 23 '19 at 14:24
  • 1
    Since we are talking about traits... `'static` is probably missing somewhere? – Matthieu M. Jan 23 '19 at 14:25
  • @MatthieuM. Why is a static lifetime required for the argument of `partial_cmp` but not for `cmp`? – Peter Hall Jan 23 '19 at 14:36
  • @PeterHall: I have no idea, but I think that this may be the clue behind the "expected std::cmp::Eq found std::cmp::Eq", one has a `'static` lifetime that is not shown, while the other doesn't. I am certainly looking forward to the answer of this question :D – Matthieu M. Jan 23 '19 at 14:46
  • `fn partial_cmp(&self, other: &(dyn SimpleOrder + 'static)) -> Option` works ;) – hellow Jan 23 '19 at 14:50
  • A much simpler reproduction of the issue: https://play.rust-lang.org/?version=nightly&mode=debug&edition=2018&gist=b9651de4c79c6f68b0d155f4e72dfa97 – Peter Hall Jan 23 '19 at 14:53
  • Using `Self` instead of `dyn SimpleOrder` also works. I suspect this has something to do with lifetime elision. – trent Jan 23 '19 at 14:55
  • @trentcl: Probably. I guess `Self` is deduced to be `dyn SimpleOrder + 'static`, and `dyn SimpleOrder`, as an argument, is treated as "potentially something else entirely". – Matthieu M. Jan 23 '19 at 14:56
  • Yes, `fn f(&self, o: &dyn Trt)` is equal to `fn<'a> f(&self, o: &'a (dyn Trt + 'a)`. But @PeterHall note that your reproduction actually produces a different (albeit similar) issue. The issue in orlp's code is triggered by rust typechecking against the signature of `cmp` of the `Ord` trait instead of the actual implementation. Otherwise his code works perfectly fine. – Chronial Jan 23 '19 at 15:34
  • @Chronial I think it's the same. What I narrowed in on was the fact that one method calls another. The issue doesn't occur when they don't call each other. – Peter Hall Jan 23 '19 at 15:40
  • @PeterHall if you copy the `cmp` implementation into the `SimpleOrder` trait, rename it to `mycmp` and then call that in `partial_cmp`, the code will compile. – Chronial Jan 23 '19 at 15:45

1 Answers1

22

Trait object types have an associated lifetime bound, but it can be omitted. A full trait object type is written dyn Trait + 'a (when behind a reference, parentheses must be added around it: &(dyn Trait + 'a)).

The tricky part is that when a lifetime bound is omitted, the rules are a bit complicated.

First, we have:

impl PartialOrd for dyn SimpleOrder {

Here, the compiler infers + 'static. Lifetime parameters are never introduced on impl blocks (as of Rust 1.32.0).

Next, we have:

    fn partial_cmp(&self, other: &dyn SimpleOrder) -> Option<Ordering> {

The type of other is inferred to be &'b (dyn SimpleOrder + 'b), where 'b is an implicit lifetime parameter introduced on partial_cmp.

    fn partial_cmp<'a, 'b>(&'a self, other: &'b (dyn SimpleOrder + 'b)) -> Option<Ordering> {

So now we have that self has type &'a (dyn SimpleOrder + 'static) while other has type &'b (dyn SimpleOrder + 'b). What's the problem?

Indeed, cmp doesn't give any error, because its implementation doesn't require that the lifetime of the two trait objects be equal. Why does partial_cmp care, though?

Because partial_cmp is calling Ord::cmp. When type checking a call to a trait method, the compiler checks against the signature from the trait. Let's review that signature:

pub trait Ord: Eq + PartialOrd<Self> {
    fn cmp(&self, other: &Self) -> Ordering;

The trait requires that other be of type Self. That means that when partial_cmp calls cmp, it tries to pass a &'b (dyn SimpleOrder + 'b) to a parameter that expects a &'b (dyn SimpleOrder + 'static), because Self is dyn SimpleOrder + 'static. This conversion is not valid ('b cannot be converted to 'static), so the compiler gives an error.

So then, why is it valid to set the type of other to &'b (dyn SimpleOrder + 'b) when implementing Ord? Because &'b (dyn SimpleOrder + 'b) is a supertype of &'b (dyn SimpleOrder + 'static), and Rust lets you replace a parameter type with one of its supertypes when implementing a trait method (it makes the method strictly more general, even though it's apparently not used much in type checking).


In order to make your implementation as generic as possible, you should introduce a lifetime parameter on the impls:

use std::cmp::Ordering;

pub trait SimpleOrder {
    fn key(&self) -> u32;
}

impl<'a> PartialOrd for dyn SimpleOrder + 'a {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        Some(self.cmp(other))
    }
}

impl<'a> Ord for dyn SimpleOrder + 'a {
    fn cmp(&self, other: &Self) -> Ordering {
        self.key().cmp(&other.key())
    }
}

impl<'a> PartialEq for dyn SimpleOrder + 'a {
    fn eq(&self, other: &Self) -> bool {
        self.key() == other.key()
    }
}

impl<'a> Eq for dyn SimpleOrder + 'a {}
Francis Gagné
  • 60,274
  • 7
  • 180
  • 155
  • 5
    That makes sense. I do believe that the error message generated by Rust should be improved though - that this is the issue isn't *at all* clear from the error message. – orlp Jan 23 '19 at 15:23
  • "So now we have that self has type &(dyn SimpleOrder + 'a) while other has type &(dyn SimpleOrder + 'static)." – this should be the other way round, right? – Chronial Jan 23 '19 at 15:23
  • @Chronial oops, fixed. – Francis Gagné Jan 23 '19 at 15:24
  • 3
    The thing that is surprising to me here is that calling `SimpleOrder::cmp(self, other)` does not check against the signature of `SimpleOrder::cmp` (which would succeed), but against the signature of `Ord::cmp` (which fails). – Chronial Jan 23 '19 at 15:26
  • 1
    @Chronial: I think this would warrant its own (separate) question; notably, I guess it would be possible to trigger the behavior without `dyn Trait`, just exploiting sub-typing with regular types containing references. – Matthieu M. Jan 23 '19 at 15:53
  • What surprises *me* is that this doesn't match the behavior of `Box` (it's not treated as a free parameter, but always as `'static`). There must be something special about references. Did you refer to any particular documentation to write this answer? I can't find anything that explicitly describes how lifetime parameters on trait objects are inferred. – trent Jan 23 '19 at 16:23
  • @trentcl No, I just experimented in the playground. I noticed in particular that introducing the explicit lifetime in `partial_cmp`'s signature (as written in my answer) didn't change the compiler's behavior, so I assumed both forms were equivalent. – Francis Gagné Jan 23 '19 at 17:29
  • 2
    @trentcl I remember reading somewhere that `&'a T` is equal to `&'a (T + 'a)`. That does also fit the behavior of `Box` (non-references are `'static`). Ah, found the reference: https://doc.rust-lang.org/reference/lifetime-elision.html#default-trait-object-lifetimes – Chronial Jan 24 '19 at 04:51
  • @Chronial Thanks for the reference! I've incorporated it into my answer and edited it accordingly. – Francis Gagné Jan 24 '19 at 21:47