Mysterious lifetime issue while implementing trait for dyn object
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?
rust traits lifetime
|
show 6 more comments
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?
rust traits lifetime
2
This is mysterious!
– Peter Hall
Jan 23 at 14:24
1
Since we are talking about traits...'static
is probably missing somewhere?
– Matthieu M.
Jan 23 at 14:25
@MatthieuM. Why is a static lifetime required for the argument ofpartial_cmp
but not forcmp
?
– Peter Hall
Jan 23 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 at 14:46
fn partial_cmp(&self, other: &(dyn SimpleOrder + 'static)) -> Option<Ordering>
works ;)
– hellow
Jan 23 at 14:50
|
show 6 more comments
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?
rust traits lifetime
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?
rust traits lifetime
rust traits lifetime
edited Jan 23 at 14:24
Peter Hall
17.3k74393
17.3k74393
asked Jan 23 at 14:14
orlporlp
68k27162249
68k27162249
2
This is mysterious!
– Peter Hall
Jan 23 at 14:24
1
Since we are talking about traits...'static
is probably missing somewhere?
– Matthieu M.
Jan 23 at 14:25
@MatthieuM. Why is a static lifetime required for the argument ofpartial_cmp
but not forcmp
?
– Peter Hall
Jan 23 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 at 14:46
fn partial_cmp(&self, other: &(dyn SimpleOrder + 'static)) -> Option<Ordering>
works ;)
– hellow
Jan 23 at 14:50
|
show 6 more comments
2
This is mysterious!
– Peter Hall
Jan 23 at 14:24
1
Since we are talking about traits...'static
is probably missing somewhere?
– Matthieu M.
Jan 23 at 14:25
@MatthieuM. Why is a static lifetime required for the argument ofpartial_cmp
but not forcmp
?
– Peter Hall
Jan 23 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 at 14:46
fn partial_cmp(&self, other: &(dyn SimpleOrder + 'static)) -> Option<Ordering>
works ;)
– hellow
Jan 23 at 14:50
2
2
This is mysterious!
– Peter Hall
Jan 23 at 14:24
This is mysterious!
– Peter Hall
Jan 23 at 14:24
1
1
Since we are talking about traits...
'static
is probably missing somewhere?– Matthieu M.
Jan 23 at 14:25
Since we are talking about traits...
'static
is probably missing somewhere?– Matthieu M.
Jan 23 at 14:25
@MatthieuM. Why is a static lifetime required for the argument of
partial_cmp
but not for cmp
?– Peter Hall
Jan 23 at 14:36
@MatthieuM. Why is a static lifetime required for the argument of
partial_cmp
but not for cmp
?– Peter Hall
Jan 23 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 at 14:46
@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 at 14:46
fn partial_cmp(&self, other: &(dyn SimpleOrder + 'static)) -> Option<Ordering>
works ;)– hellow
Jan 23 at 14:50
fn partial_cmp(&self, other: &(dyn SimpleOrder + 'static)) -> Option<Ordering>
works ;)– hellow
Jan 23 at 14:50
|
show 6 more comments
1 Answer
1
active
oldest
votes
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 impl
s:
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 {}
4
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 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 at 15:23
3
The thing that is surprising to me here is that callingSimpleOrder::cmp(self, other)
does not check against the signature ofSimpleOrder::cmp
(which would succeed), but against the signature ofOrd::cmp
(which fails).
– Chronial
Jan 23 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 withoutdyn Trait
, just exploiting sub-typing with regular types containing references.
– Matthieu M.
Jan 23 at 15:53
2
@trentcl I remember reading somewhere that&'a T
is equal to&'a (T + 'a)
. That does also fit the behavior ofBox
(non-references are'static
). Ah, found the reference: doc.rust-lang.org/reference/…
– Chronial
Jan 24 at 4:51
|
show 4 more comments
Your Answer
StackExchange.ifUsing("editor", function () {
StackExchange.using("externalEditor", function () {
StackExchange.using("snippets", function () {
StackExchange.snippets.init();
});
});
}, "code-snippets");
StackExchange.ready(function() {
var channelOptions = {
tags: "".split(" "),
id: "1"
};
initTagRenderer("".split(" "), "".split(" "), channelOptions);
StackExchange.using("externalEditor", function() {
// Have to fire editor after snippets, if snippets enabled
if (StackExchange.settings.snippets.snippetsEnabled) {
StackExchange.using("snippets", function() {
createEditor();
});
}
else {
createEditor();
}
});
function createEditor() {
StackExchange.prepareEditor({
heartbeatType: 'answer',
autoActivateHeartbeat: false,
convertImagesToLinks: true,
noModals: true,
showLowRepImageUploadWarning: true,
reputationToPostImages: 10,
bindNavPrevention: true,
postfix: "",
imageUploader: {
brandingHtml: "Powered by u003ca class="icon-imgur-white" href="https://imgur.com/"u003eu003c/au003e",
contentPolicyHtml: "User contributions licensed under u003ca href="https://creativecommons.org/licenses/by-sa/3.0/"u003ecc by-sa 3.0 with attribution requiredu003c/au003e u003ca href="https://stackoverflow.com/legal/content-policy"u003e(content policy)u003c/au003e",
allowUrls: true
},
onDemand: true,
discardSelector: ".discard-answer"
,immediatelyShowMarkdownHelp:true
});
}
});
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
StackExchange.ready(
function () {
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fstackoverflow.com%2fquestions%2f54329200%2fmysterious-lifetime-issue-while-implementing-trait-for-dyn-object%23new-answer', 'question_page');
}
);
Post as a guest
Required, but never shown
1 Answer
1
active
oldest
votes
1 Answer
1
active
oldest
votes
active
oldest
votes
active
oldest
votes
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 impl
s:
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 {}
4
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 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 at 15:23
3
The thing that is surprising to me here is that callingSimpleOrder::cmp(self, other)
does not check against the signature ofSimpleOrder::cmp
(which would succeed), but against the signature ofOrd::cmp
(which fails).
– Chronial
Jan 23 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 withoutdyn Trait
, just exploiting sub-typing with regular types containing references.
– Matthieu M.
Jan 23 at 15:53
2
@trentcl I remember reading somewhere that&'a T
is equal to&'a (T + 'a)
. That does also fit the behavior ofBox
(non-references are'static
). Ah, found the reference: doc.rust-lang.org/reference/…
– Chronial
Jan 24 at 4:51
|
show 4 more comments
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 impl
s:
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 {}
4
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 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 at 15:23
3
The thing that is surprising to me here is that callingSimpleOrder::cmp(self, other)
does not check against the signature ofSimpleOrder::cmp
(which would succeed), but against the signature ofOrd::cmp
(which fails).
– Chronial
Jan 23 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 withoutdyn Trait
, just exploiting sub-typing with regular types containing references.
– Matthieu M.
Jan 23 at 15:53
2
@trentcl I remember reading somewhere that&'a T
is equal to&'a (T + 'a)
. That does also fit the behavior ofBox
(non-references are'static
). Ah, found the reference: doc.rust-lang.org/reference/…
– Chronial
Jan 24 at 4:51
|
show 4 more comments
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 impl
s:
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 {}
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 impl
s:
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 {}
edited Jan 24 at 21:46
answered Jan 23 at 15:17
Francis GagnéFrancis Gagné
33.3k27183
33.3k27183
4
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 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 at 15:23
3
The thing that is surprising to me here is that callingSimpleOrder::cmp(self, other)
does not check against the signature ofSimpleOrder::cmp
(which would succeed), but against the signature ofOrd::cmp
(which fails).
– Chronial
Jan 23 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 withoutdyn Trait
, just exploiting sub-typing with regular types containing references.
– Matthieu M.
Jan 23 at 15:53
2
@trentcl I remember reading somewhere that&'a T
is equal to&'a (T + 'a)
. That does also fit the behavior ofBox
(non-references are'static
). Ah, found the reference: doc.rust-lang.org/reference/…
– Chronial
Jan 24 at 4:51
|
show 4 more comments
4
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 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 at 15:23
3
The thing that is surprising to me here is that callingSimpleOrder::cmp(self, other)
does not check against the signature ofSimpleOrder::cmp
(which would succeed), but against the signature ofOrd::cmp
(which fails).
– Chronial
Jan 23 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 withoutdyn Trait
, just exploiting sub-typing with regular types containing references.
– Matthieu M.
Jan 23 at 15:53
2
@trentcl I remember reading somewhere that&'a T
is equal to&'a (T + 'a)
. That does also fit the behavior ofBox
(non-references are'static
). Ah, found the reference: doc.rust-lang.org/reference/…
– Chronial
Jan 24 at 4:51
4
4
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 at 15:23
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 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 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 at 15:23
3
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 at 15:26
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 at 15:26
1
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 at 15:53
@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 at 15:53
2
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: doc.rust-lang.org/reference/…– Chronial
Jan 24 at 4:51
@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: doc.rust-lang.org/reference/…– Chronial
Jan 24 at 4:51
|
show 4 more comments
Thanks for contributing an answer to Stack Overflow!
- Please be sure to answer the question. Provide details and share your research!
But avoid …
- Asking for help, clarification, or responding to other answers.
- Making statements based on opinion; back them up with references or personal experience.
To learn more, see our tips on writing great answers.
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
StackExchange.ready(
function () {
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fstackoverflow.com%2fquestions%2f54329200%2fmysterious-lifetime-issue-while-implementing-trait-for-dyn-object%23new-answer', 'question_page');
}
);
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
2
This is mysterious!
– Peter Hall
Jan 23 at 14:24
1
Since we are talking about traits...
'static
is probably missing somewhere?– Matthieu M.
Jan 23 at 14:25
@MatthieuM. Why is a static lifetime required for the argument of
partial_cmp
but not forcmp
?– Peter Hall
Jan 23 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 at 14:46
fn partial_cmp(&self, other: &(dyn SimpleOrder + 'static)) -> Option<Ordering>
works ;)– hellow
Jan 23 at 14:50