Structs with undefined/universal lifetimes (not higher-ranked)

Hey folks,

consider this code:

use std::marker::PhantomData;

struct HelperContainer<'helper, T> {
    field: T,
    _marker: PhantomData<&'helper ()>,
}

fn buildHelper<'helper, T>(v: T) -> HelperContainer<'helper, impl Sized + use<'helper, T>> {
    HelperContainer {
        field: v,
        _marker: PhantomData,
    }
}

fn main() {
    let x = buildHelper("abc");
}

This compiles but I think it shouldn't or am I wrong? The reason why I think that is because the lifetime 'helper in buildHelper doesn't depend on the input - it can be everything. Nevertheless it gets captures in the RPIT which stands for an actual field in the container.

Regards
keks

The 'helper lifetime for buildHelper("abc") is 'static:

'static can also be shrunk to any shorter lifetime. The only problem (that I am aware of) is when there are two borrows with non-overlapping lifetimes:

1 Like

It can be anything, not everything at once. The caller can choose:

let x = buildHelper::<'static, _>("abc");

In practice, they probably won't, and it will be inferred. Sometimes it will have to infer to something specific or with a lower bound...

fn constrain_lifetime<'a>() -> impl Sized + 'static {
    // error: lifetime may not live long enough
    // buildHelper::<'a, _>("")
    
    // Inferred to be `'static`
    buildHelper::("")
}

...but if it doesn't, that can be fine. The borrow checker doesn't generally assign specific lifetimes. It proves[1] that all required lifetime constraints can be met (without conflicting with the uses of referents, like moving something that's still borrowed).

In this case:

fn main() {
    let x = buildHelper("abc");
}

there are no conflicts, no matter what the lifetime is. So it doesn't really matter what the lifetime is. So long as the borrow checker can prove that at least one solution exists, the borrow check passes.

It's not an RPIT only issue. This also compiles and is usable:

fn example<'a>() -> &'a str {
    "hi!"
}

It's analogous to as if you did this.

// impl Sized + use<'helper, T>
trait DefineAnOpaque<'helper, T> {
    priv type Ty: Sized;
}

impl DefineAnOpaque<'helper, T> for () {
    // Same for all lifetimes but so what, still a valid definition
    // Also, a non-breaking change to make it depend on `'helper` in the future.
    priv type Ty = T;
}

Even though you could do this.

// impl Sized + use<T>
trait DefineAnOpaque<T> {
    priv type Ty: Sized;
}

impl DefineAnOpaque<T> for () {
    // Now it's a breaking change to rely on the lifetime in the future...
    priv type Ty = T;
}

  1. attempts to prove ↩︎

1 Like

That's not due to the 'helper lifetime though:

fn main() {
    let long = String::from("long");
    let mut x = buildHelper(&long);
    {
        let short = String::from("short");
        // This is fine
        let y = buildHelper::<'static>(&short);
        // This is the part that errors
        x = y;
    }
}

It's because the opaque in x drops at the end of main, but y captured a reference to something that drops at the end of the inner block. If you rule out the possibility of drops...

fn buildCopiableHelper<'helper, T: Copy>(v: T)
    -> HelperContainer<'helper, impl Copy + Sized + use<'helper, T>> 
{
    HelperContainer {
        field: v,
        _marker: PhantomData,
    }
}

fn main() {
    let long = String::from("long");
    let mut x = buildCopiableHelper(&long);
    {
        let short = String::from("short");
        let y = buildCopiableHelper::<'static>(&short);
        x = y;
    }
}

It compiles. (The compiler exploits a "Copy means no drop" rule.)

The T passed to buildCopiableHelper have to be the same for the types to line up, which means the borrows of long and short have to have the same lifetime, but there's nothing preventing that anymore -- without the drop, there are no uses causing the lifetime to be active after x = y.


use<..> lifetimes often work more at the type level than the "I have a field" level, like my trait analogy above. There are various reasons that the lifetimes can't be completely erased, even when there's something like a 'static bound which makes a lot of borrow check errors go away (since Rust 1.75). I wrote more about that in a prior thread.

2 Likes

Yeah, I'm unsure what the question is. Any type/lifetime parameter in a polymorphic function doesn't have to be "used" by the inputs nor output of a function. The whole point of PhantomData is that it captures only the type information associated with it which "normal" types can't do without it; thus this doesn't seem to be related to RPIT but at most PhantomData. PhantomData is treated specially in that it only captures type information related to the type/lifetime parameters and doesn't actually need to store any runtime information.

#![feature(never_type)]
use core::marker::PhantomData;
/// `bool` doesn't depend on any of the type/lifetime parameters; however since the lifetime
/// parameters are all "late-bound", we can never explicitly pass in a lifetime argument.
fn foo<'a, 'b, T, T2>(x: u32) -> bool {
    true
}
/// _Almost_ the same signature as above except we make the lifetime parameters "early-bound" (just like the
/// "normal" type parameters `T` and `T2`); thus we _can_ pass in lifetime arguments explicitly.
fn bar<'a: 'a, 'b: 'b, T, T2>(x: u32) -> bool {
    true
}
/// Again, similar to above except only the second lifetime parameter is "early-bound". Here we are allowed
/// _for now_ to pass in a lifetime parameter for it, but in the future the compiler won't allow you to pass
/// in _any_ lifetime arguments if at least one is "late-bound".
fn fizz<'a, 'b: 'b, T, T2>(x: u32) -> bool {
    true
}
struct A<'a, 'b, T, T2>(PhantomData<fn() -> (&'a (), &'b (), T, T2)>);
fn a<'a, 'b, 'c, T, T2, T3>(x: T3) -> A<'a, 'b, T, T2> {
    A(PhantomData)
}
fn main() {
    _ = foo::<!, !>(10);
    // Uncommenting below won't compile
    // _ = foo::<'_, 'static, (), ()>(10);
    _ = bar::<'static, '_, !, !>;
    _ = fizz::<(), ()>;
    // This compiles _today_ but won't in the future.
    _ = fizz::<'static, !, !>;
    _ = a::<!, !, _>(10);
}

Notice we can even pass in "uninhabited" types like ! without issue.

1 Like

That’s true, but the thing that is unique about a lifetime parameter in this position is that the call to the function need not provide an argument for that lifetime parameter, even implicitly. If you do that with a type parameter, or const generic parameter, you will get a “type annotations needed” error.

fn ok<'a>() {}
fn not_ok_1<T>() {}
fn not_ok_2<const N: usize>() {}

fn main() {
    ok();
    not_ok_1(); // error[E0282]: type annotations needed
    not_ok_2(); // error[E0284]: type annotations needed
}

The lifetime parameter is truly unconstrained, but the type parameter and the const parameter must not be. This is not just an arbitrary decision, but follows from the fact that lifetimes are erased and other generics are not — the machine code’s behavior is not allowed to depend on which lifetime is in use, therefore there is no need to require the programmer to specify which lifetime is use.

4 Likes

omg... it totally slipped my mind that type inference could just infer here 'static as generic parameter. I was focusing completely on the input parameters and could no longer see the forest for the trees. :joy: Thank you so much for your help guys!

Yeah, that's a pretty important difference I should have also highlighted.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.