1

The code wants to constrain x and y to the same lifetime and uses a function to do so. But the compiler does not error when y is dropped. Does this mean lifetime bounds do not work between function arguments, irrespective of function return value?

static S: String = String::new();

// `x` and `y` have the same lifetime
fn foo<'a>(x: &'a String, y: &'a String) -> &'static String {
    if x == y {
        &S
    } else {
        &S
    }
}

fn main() {
    let z;
    let x = &"x".to_string();
    {
        let y = &"y".to_string();
        z = foo(x, y);
    } // drops `y`
    dbg!(x);
    dbg!(z);
}
TSK
  • 509
  • 1
  • 9
  • because in your code the return value is not bound to `'a` so why would the compiler complain? you are returning a `'static` which is a stricter bound than `'a` and has the lifetime of the program— on the other hand if foo returned a `&'a String' you would get the error that I think you are expecting. – Ahmed Masud Nov 05 '22 at 15:33

1 Answers1

4

Requiring "the same" lifetime just means that the compiler must find some single lifetime that matches the scopes of objects referenced by x and y. It doesn't mean that they have to have identical scopes.

Since your function doesn't use the lifetime in return position, and they are non-mutable, it is not useful. A useful argument-only lifetime will typically involve mutation - for example:

fn append<'a>(v: &mut Vec<&'a str>, s: &'a str) {
    v.push(s);
}

If you tried to omit the lifetime here, the function wouldn't compile, because then it would allow appending short-lived references to a vector that expects e.g. static ones.

user4815162342
  • 141,790
  • 18
  • 296
  • 355
  • This looks reasonable overall, but I still don't get the exact semantics of `'a` in code. I'm wondering why you are able to satisfy the compiler with only one but not two lifetime parameters. When you write `&'a str`, does this mean the lifetime of the reference is longer, shorter or exactly `'a`? – TSK Nov 05 '22 at 17:16
  • 1
    @TSK: Technically `&'a str` means that the lifetime of that reference is exactly `'a`. But then if `'a` outlives `'b` then `&'a str` can be trivially coerced into `&'b str` (one is a sub-type of the other). So in practice you can think of `&'a str` as a reference living for at least `'a` and everything will usually fit just the same with less words. – rodrigo Nov 05 '22 at 17:58
  • @rodrigo Following your explanation, how would you express `fn append<'a>(v: &mut Vec<&'a str>, s: &'a str)` in plain English? The signature tells you there are two occurrences of references with lifetimes, but how do you establish the constraints between them? – TSK Nov 05 '22 at 18:04
  • @TSK I would express it as "there must exist a lifetime `'a` common to references stored in `v` and to `s`". In the context of calling the function, it means "the caller must be able to come up with a lifetime...". In other words, the compiler is free to coerce different longer lifetimes to single shorter one (which overlaps them), if such can be found. – user4815162342 Nov 05 '22 at 18:54
  • @user4815162342 "there must exist a lifetime 'a common to references stored in v and to s": wouldn't this be satisfied trivially at the call site of `v.push(s)`? How does this constrain more than that? – TSK Nov 05 '22 at 19:00
  • @TSK Ah, but the call site of `v.push(s)` is a different thing, it's _inside_ the body of the function, so it's not relevant when calling `append()`. The borrow checker doesn't look at the function's implementation when type/borrow checking, it only looks at its signature - see the last paragraph of [this answer](https://stackoverflow.com/a/67653296/1600898) for a more detailed discussion. – user4815162342 Nov 05 '22 at 19:11
  • Assuming "the caller must be able to come up with a lifetime...", I still don't see how a constraint such as "what lifetime must >= what lifetime" is established. If I remove all lifetime parameters, the compiler complains "argument requires that `'1` must outlive `'2`". What I still don't see is how these lifetime parameters satisfy the compiler. It looks to me that it's `v.push(s)` asking for this constraint. – TSK Nov 05 '22 at 19:36
  • 1
    The declaration of the function alone is the _inteface_ between what is inside the function and what is outside. Indeed, `v.push(s);` constraints that the lifetime of `s` and that of the value inside `v` must be the same, and that will _just work_ with local variables. But if they both are function parameters, then the lifetime relationship must be explicit: it will never be affected by the code inside the function. – rodrigo Nov 05 '22 at 19:46
  • What sounds reasonable to me is `v.push(s)` requires the type of `s` be a subtype of the element type of `v`. So `fn append<'a, 'b: 'a>(v: &mut Vec<&'a str>, s: &'b str)` looks good to me. I'm wondering how you can express this constraint using only one lifetime parameter. – TSK Nov 05 '22 at 19:47
  • @rodrigo So I guess, in `fn append<'a>(v: &mut Vec<&'a str>, s: &'a str)`: 1) The function signature by itself doesn't introduce any constraint. 2) The constraint is required by `v.push(s)`. 3) We need the lifetime parameter simply because it's part of the function interface which requires things being explicit. – TSK Nov 05 '22 at 20:00
  • 1) Actually it does add a constraint, not easy to build a code that won't compile because of it because, well, the compiler really wants to compile your code. Anyway, there are other consequences of a lifetime othen than not compiling your function [(playground)](https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=1798cc73201817b77a61edd4c1dee56c). – rodrigo Nov 05 '22 at 20:48
  • 2 and 3) Yes, without that constraint the `v.push(s)` will not work. But without that constraint, my playground above would compile and you wouldn't want that. – rodrigo Nov 05 '22 at 20:50
  • @TSK "So fn `append<'a, 'b: 'a>(v: &mut Vec<&'a str>, s: &'b str)` looks good to me" - This constraint, while technically correct, is also (in this case) needlessly complicated. The part I believe you're missing is that the simpler declaration works is because the compiler is not required to plug variable scopes into the lifetime `'a`; it is allowed to pass a sub-lifetime, as long as it's a sub-lifetime of both the scopes in the caller. – user4815162342 Nov 05 '22 at 22:18
  • 2
    @TSK Maybe an analogy with subclasses would help - imagine a language that supports both subtyping and generics. Now let's say you want to create the `append()` function that allows populating the vector with correct types. Something like `fn append(v: &mut Vec, s: B)` won't accept the body of the function because `A` and `B` are unrelated, and the implementation contains `v.push(s)`. On the other hand, `fn append(v: &mut Vec, s: B)` compiles because it establishes that `B` is a subtype of `A`. **However**, `fn append(v: &mut Vec, s: A)` **also works**. – user4815162342 Nov 05 '22 at 22:24
  • @TSK This is because if you call the function as `append(vec_of_mammals, some_cow)`, the compiler will figure out that your cow is-a mammal, and will call `append`, silently coercing `some_cow` to `Mammal`. This is what Rust is doing with the lifetimes when you declare `append()` with a single lifetimes. – user4815162342 Nov 05 '22 at 22:24
  • @user4815162342: This analogy with classes is just right. IExcept that `'static` has no equivalence in the class world. On first sight it looks like `'static` should be equivalent to `object`, that is the superclass of all the classes. But actually it is a _subclass_ of all the objects! And unfortunately that has no equivalence with OO classes (except maybe the `null` reference?). – rodrigo Nov 07 '22 at 20:48
  • @rodrigo Yeah, the analogy is stretched, also because in this case the compiler is allowed to up and *invent* a class (scope subset, aka lifetime) out of existing ones. But despite all the flaws, I still hope the analogy provides a fresh look from a different angle to help the OP reach the needed a-ha. – user4815162342 Nov 07 '22 at 23:09