Skip to content

Lifetime issues caused by implicitly choosing the wrong implementation of Borrow are incomprehensible #136793

Open
@meithecatte

Description

@meithecatte

Code

use std::collections::BTreeSet;

pub fn find_static<'a>(map: &BTreeSet<&'static str>, key: &'a str) -> &'static str {
    *map.get(&key).unwrap()
}

Current output

error: lifetime may not live long enough
 --> src/lib.rs:4:5
  |
3 | pub fn find_static<'a>(map: &BTreeSet<&'static str>, key: &'a str) -> &'static str {
  |                    -- lifetime `'a` defined here
4 |     *map.get(&key).unwrap()
  |     ^^^^^^^^^^^^^^^^^^^^^^^ returning this value requires that `'a` must outlive `'static`

Desired output

error: lifetime may not live long enough
 --> src/lib.rs:4:5
  |
3 | pub fn find_static<'a>(map: &BTreeSet<&'static str>, key: &'a str) -> &'static str {
  |                    -- lifetime `'a` defined here
4 |     *map.get(&key).unwrap()
  |     ^^^^^^^^^^^^^^^^^^^^^^^ returning this value requires that `'a` must outlive `'static`
note: `'static` in the type of `map` narrowed to `'a` to match `key`
note: lifetimes must match due to the implementation of `Borrow` being used
 --> rustc-src/std/blah/borrow.rs:12345
   impl<T> Borrow<T> for T { ... }
   ^^^^^^^^^^^^^^^^^^^^^^^
help: consider removing the borrow here
  |
4 |     *map.get(&key).unwrap()
  |              -
  |             ( key)

Rationale and extra context

This is a very confusing case that definitely needs more attention in the diagnostics in order to be understandable by a typical Rust developer. After much head-scratching, I have arrived at the following explanation of why this works like that:

  • when doing get(key), the trait bounds require &'static str: Borrow<str>, which resolves through impl<T> Borrow<T> for &T and all is good with the world
  • when doing get(&key), the trait bounds require &'static str: Borrow<&'a str>, which resolves through impl<T> Borrow<T> for T and unifies 'static with 'a, which means that the compiler uses variance to shrink the 'static in &BTreeSet<&'static str> to &BTreeSet<&'a str>

Other cases

If the key is a lifetime-parametrized struct, there is no easy way of achieving the same thing:

use std::collections::BTreeMap;

#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
struct Meow<'a> {
    pub name: &'a str,
    pub index: usize,
}

pub fn launder_lifetime<'a>(map: &BTreeMap<Meow<'static>, usize>, key: Meow<'a>) -> Meow<'static> {
    // same error as above
    *map.get_key_value(&key).unwrap().0
}

The best way of making this compile that I know of involves this monstrosity:

use std::borrow::Borrow;
use std::collections::BTreeMap;

#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
struct Meow<'a> {
    pub name: &'a str,
    pub index: usize,
}

#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
struct UglyWorkaround<'a>(Meow<'a>);

impl<'a, 'b: 'a> Borrow<Meow<'a>> for UglyWorkaround<'b> {
    fn borrow(&self) -> &Meow<'a> {
        &self.0
    }
}

pub fn launder_lifetime<'a>(map: &BTreeMap<UglyWorkaround<'static>, usize>, key: Meow<'a>) -> Meow<'static> {
    map.get_key_value(&key).unwrap().0.0
}

Guiding the user towards that would probably have to involve an example in the docs...

Rust Version

rustc 1.86.0-nightly (43ca9d18e 2025-02-08)
binary: rustc
commit-hash: 43ca9d18e333797f0aa3b525501a7cec8d61a96b
commit-date: 2025-02-08
host: x86_64-unknown-linux-gnu
release: 1.86.0-nightly
LLVM version: 19.1.7

Metadata

Metadata

Assignees

No one assigned

    Labels

    A-diagnosticsArea: Messages for errors, warnings, and lintsT-compilerRelevant to the compiler team, which will review and decide on the PR/issue.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions