Why does a where clause confuse lifetime analysis?


#1

Or maybe it’s just me who’s confused. I’m running into problems with this code:

pub struct Handle<T>(pub T);

pub fn p(x: Handle<&u64>)
{
    println!("{}", *x.0);
}

impl<'a> From<&'a u64> for Handle<&'a u64>
{
    fn from(x: &'a u64) -> Handle<&'a u64> {
        Handle(x)
    }
}

pub fn convert(x: &u64) -> Handle<&u64> {
    x.into()
}

pub fn test<'b>()
where
    Handle<&'b u64>: From<&'b u64>,
{
    let i: u64 = 15;
    let r: &u64 = &i;
    p(r.into());
}

Which complains that r's lifetime doesn’t contain 'b. I don’t understand that at all: 'b has nothing to do with r! In fact, removing the where clause entirely works, as does using convert(r)instead of r.into(). However, that’s an ugly solution for a problem I don’t really understand in the first place.


#2

By making test() generic over a lifetime 'b, you signal to the compiler that the caller may pick any lifetime of their choosing. Kind of like with fn test<T>(), the caller may choose any type they want for T.

However, if you look at the body of test(), you see that this cannot work: The lifetime of &i is fixed because i lives on the stack, and hence the caller cannot choose any lifetime they want.

Long story short: You’re right, 'b has nothing to do with r and hence, it’s wrong to let the caller of test() choose a 'b.


#3

To add to @troiganto you want an HRTB bound:

pub fn test()
where
    for<'b> Handle<&'b u64>: From<&'b u64>,

I like to link this for a good explanation of the difference between the two.


#4

Sorry, I don’t understand: how is the lifetime 'b that is chosen by the caller relevant to the code in test? It isn’t, is it?

Obviously, I’m working on code that is more complex and does require the where clause (or I’d just remove it), and a lifetime parameter.

You appear to be saying

pub fn test<'b>()
{
    let i: u64 = 15;
    let r: &u64 = &i;
    p(r.into());
}

shouldn’t compile. It does, though!

Translated into English, I think my code is saying “choose any lifetime. I’ll ignore it, print 15, and return.” The extra where clause would mean “choose any lifetime for which &'b u64 is convertible to Handle<&'b u64>”, which is trivially true.


#5

The problem with your original code is 'b is used in the where clause to indicate when such a conversion is possible - the 'b generic lifetime parameter of the function is the same one as in the where clause bounds. So you’re basically telling the compiler that the conversion is only valid for user-passed references, not ones you take from values inside the function.

The rest of your code is fine - your where clause bound, however, instructs the wrong thing.


#6

Sorry, but I still don’t get it. Let’s try with slightly expanded code:

pub struct Handle<T>(pub T);

pub fn p(x: Handle<&u64>)
{
    println!("{}", *x.0);
}

impl<'a> From<&'a u64> for Handle<&'a u64>
{
    fn from(x: &'a u64) -> Handle<&'a u64> {
        Handle(x)
    }
}

pub fn convert(x: &u64) -> Handle<&u64> {
    x.into()
}

pub fn test<'b,T>(x: T) -> Handle<&'b u64>
    where Handle<&'b u64>: From<T>
{
    let i: u64 = 15;
    let r: &u64 = &i;
    p(r.into());
    x.into()
}

fn main() {
    let i: u64 = 16;
    let h: Handle<&u64> = test(&i);
    p(h);
}

Here, I want test to convert its argument, but also print 15. T can be converted to &'b u64 because of the where clause. r can be converted to &u64, with the same lifetime as r, because From is implemented for all lifetimes. However, r.into() appears to attempt to use the conversion specified in the where clause rather than the generic conversion available for all lifetimes.

Specifying an extra for<'c> Handle<&'c u64>: From<&'c u64> results in an error message which is downright baffling: "cannot infer type for Handle<&'b u64>". But that is a type! What is rust trying to infer?


#7

Let’s take a step back for a second :slight_smile:

pub fn test<'b,T>(x: T) -> Handle<&'b u64>
    where Handle<&'b u64>: From<T>
{
    let i: u64 = 15;
    let r: &u64 = &i;
    p(r.into());
    x.into()
}

Why are you specifying the where clause bound like that? The compiler treats where clause bounds very aggressively - it can “forget” that other impls exist (there are threads on this, which I can dig up if you’re interested). In particular, you already have

impl<'a> From<&'a u64> for Handle<&'a u64>
{
    fn from(x: &'a u64) -> Handle<&'a u64> {
        Handle(x)
    }
}

which allows a conversion from some lifetime’d &u64. This is all you need.

Your function should then be:

pub fn test<'b,T>(x: T) -> Handle<&'b u64>
    where T: Into<Handle<&'b u64>>
{
    let i: u64 = 15;
    let r: &u64 = &i;
    p(r.into());
    x.into()
}

The r.into() already works out of the box because you have the From impl - there’s no need to specify it in a where clause. The where clause, therefore, only needs to constrain the T generic parameter to allow the x.into() at the end.


#8

Thank you! I’m still confused, but I think what you said can be summarized as “use Into rather than From in where clauses, because the compiler will fail to find unrelated other .into() implementations otherwise”.

That solves my immediate issue, but it leaves me with three questions:

  1. Why? How was I supposed to guess this based on the error messages?
  2. How do I fix things if I use a custom trait for conversion rather than From? If I use MyFrom, do I have to implement MyInto to go with it just so where clauses work?
  3. What about the baffling "cannot infer type for " message?

#9

@pipcet Since you are using the into() method on x, so you should use Into as a trait bound.
If you use the method from() directly, you specify the bounds in terms of From.


#10

That’s not exactly how I’d summarize it :slight_smile:.

When you write:

pub fn test<'b,T>(x: T) -> Handle<&'b u64>
    where Handle<&'b u64>: From<T>

You’re saying “hey compiler, in this function I’m telling you that a Handle can be converted from T but the reference inside the handle must be lifetime’d by the caller specified 'b". Compiler says “ok, got it”. Then compiler sees you trying to use a conversion using a non-user supplied lifetime (cause they’re locals), and says “nope - you told me I can do the conversion only from caller supplied lifetime”. This bound “overrides” the more general From impl you have - this is what I meant by compiler “forgets” the more general impl. I’ll dig up the links to where this is discussed once I’m at a computer.

The reformulated where clause says nothing about what Handle itself can do - it only talks about what T can do. It just so happens that From and Into “pair up” due to a blanket impl existing and thus you can call into() despite only a From impl existing.


#11

@troplin If I replace r.into() with <Handle<&u64> as From<&u64>>::from(r), I get the same error.


#12

Okay, so how do I tell the compiler “hey, in this function, I’m going to convert T into a Handle, but I’ll also convert an unrelated &u64 into a Handle, and I want you to figure out the right into() to use”? As you explained, using Into rather than From works around the problem because these specific traits are paired up, but in general there won’t be an Into-like trait to go with my From-like trait.


#13

Here’s a previous discussion on compiler “forgetting” about other impls:


#14

It would be the same thing as discussed above with the reformulated where clause bound - you would only say that T can be converted, and say nothing about &u64 being convertible. The only difference is if you don’t have a blanket impl that “pairs up” the conversions, you’d need to implement the conversion manually so that into() is callable.


#15

“the same thing as discussed above with the reformulated where clause bound”

The only reformulation I see is to use Into rather than From. Are you saying that’s the only way? How would you fix this code:

pub struct Handle<T>(pub T);

pub fn p(x: Handle<&u64>)
{
    println!("{}", *x.0);
}

impl<'a> From<&'a u64> for Handle<&'a u64>
{
    fn from(x: &'a u64) -> Handle<&'a u64> {
        Handle(x)
    }
}

pub fn convert(x: &u64) -> Handle<&u64> {
    <Handle<&u64> as From<&u64>>::from(x)
}

pub fn test<'b,T>(x: T) -> Handle<&'b u64>
    where Handle<&'b u64>: From<T>
{
    let i: u64 = 15;
    let r = &i;
    p(<Handle<&u64> as From<&u64>>::from(r));
    <Handle<&u64> as From<T>>::from(x)
}

fn main() {
    let i: u64 = 16;
    let h: Handle<&u64> = test(&i);
    p(h);
}

…without translating to .into()/Into? When I use my own MyFrom trait, UFCS works, but for the real From, it doesn’t.


#16

Here’s an example (all else the same):

pub fn test<T, U>(x: T) -> Handle<U>
where
    Handle<U>: From<T>, // needed for return value conversion
    for<'b> Handle<&'b u64>: From<&'b u64>, // needed for the specific &u64 locals
{
    let i: u64 = 15;
    let r = &i;
    p(<Handle<_> as From<_>>::from(r));
    <Handle<_> as From<_>>::from(x)
}

Note we now have to return a generic Handle<U> so that the compiler isn’t confused about us specifying two different lifetime styles (i.e. caller-provided and HRTB) of &u64 conversions. Fortunately, type inference should deduce what U is in most cases.

Needless to say, if you’re defining your own traits and generic APIs, try to avoid needing bounds of the form SomeSpecificType: .... Most generic bounds are usually T: ... where the T is a generic type parameter.

Side note: you can also write the last 2 lines as:

    p(<Handle<_>>::from(r));
    <Handle<_>>::from(x)

Since we already bounded Handle to From, compiler knows that’s a callable method - there’s no real need for a as From<...> projection.


#17

Or just

p(Handle::from(r));
Handle::from(x)

or

p(r.into());
x.into()

#18

Yup, that too.

I think @pipcet specifically wanted to see how to work with just from() (i.e. pretend there’s no Into blanket impl).


#19

Using a second type parameter is a neat trick, thanks! I’ve fixed my code to use T: Into<U>, and it compiles! And I think I understand what’s going on now, though it still seems like a deficiency in the Rust compiler to me.


#20

If you follow the link chain above, there’s an open issue about this, so yeah, probably a defect.