Variance not being the whole story?

⚓ Rust    📅 2025-05-03    👤 surdeus    👁️ 5      

surdeus

Warning

This post was published 110 days ago. The information described in this article may have changed.

So I was playing with the variance rule, checking if I'm understanding anything, which turns out to be a... big NO. Consider this snippet (if you prefer playground)

#[derive(Debug)]
struct Foo<'f>(#[allow(unused)] &'f str);

impl Foo<'static> {
    fn is_static_shared(&self) {}
    fn is_static_exclusive(&mut self) {}
}

fn foo(_f: &mut Foo<'static>) {
    /*
     * This line fails to compile, as expected:
     * the fact `Foo<'static>` and `Foo<'_>` has subtype relation
     * does not imply relation between `&mut Foo<'static>` and `&mut Foo<'_>`:
     * `&mut T` is invariant over `T`
     */
    // std::mem::swap(&mut Foo(&String::from("not static")), _f);
}


fn bar<'f>(#[allow(unused)] f: &mut Foo<'f>) {
    // group A and group B cannot coexist!
    
    let mut g = Foo("static");
    
    {
        // group A
        // g.is_static_shared();
        // (&mut g).is_static_shared();
        // (&mut g).is_static_exclusive();
    }

    {
        // group B
        std::mem::swap(f, &mut g);
        std::mem::swap::<Foo<'f>>(f, &mut g);
    }
}

fn main() {
    let main = String::from("main");
    let mut main = Foo(&main);
    bar(&mut main);
    foo(&mut Foo("static"));
    dbg!(main);
}

My question is around bar:

  1. If we enable only group B, it actually compiles just fine. From a generic "prevent dangling pointers" PoV, this is completely fine: we're replacing a Foo that may borrow from some other variables, to one that borrow nothing from the context of program execution. But from a variance PoV things get tricky: we have &'_ mut Foo<'f> and &'_ mut Foo<'static>, and they are not subtype of one another by the fact &'a mut T being invariant over T, but somehow std::mem::swap thinks there's some T upon which both of the operand exclusive references agree via subtype coersion. Again, such T should not exist, no?
  2. So I thought maybe I've made some wrong assumptions. Maybe g is not Foo<'static> in the first place. So I added group A. Group A itself alone, without group B, compiles just fine. It's when enabling both group A and group B the compiler becomes confused.
  3. Last but not least, when both group A and group B are enabled, the compiler actually complains about code in A enabling some variable to escape the function body.
    • Which is kinda weird since judging by signature, the only possible place it might escape is actually group B...?
    • And then again there's the fact group B alone, without group A, compiles just fine.
error[E0521]: borrowed data escapes outside of function
  --> src/main.rs:29:9
   |
20 | fn bar<'f>(#[allow(unused)] f: &mut Foo<'f>) {
   |        --                   - `f` is a reference that is only valid in the function body
   |        |
   |        lifetime `'f` defined here
...
29 |         (&mut g).is_static_exclusive();
   |         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   |         |
   |         `f` escapes the function body here
   |         argument requires that `'f` must outlive `'static`
   |
   = note: requirement occurs because of a mutable reference to `Foo<'static>`
   = note: mutable references are invariant over their type parameter
   = help: see <https://doc.rust-lang.org/nomicon/subtyping.html> for more information about variance

For more information about this error, try `rustc --explain E0521`.
error: could not compile `playground` (bin "playground") due to 1 previous error

I'm assuming the reason here is Rust doing the variance type checking and subtype coersion not just at function call site, but also at local variable definition site, specifically the definition for g, i.e. the let mut g = Foo("static"); line before both group A and B.

  • If only group B were present, Rust treats the g as Foo<'f>, i.e. a subtype coersion happens here; it should be okay since Foo<'f> being covariant over 'f, and sunshine and rainbows.
  • If only group A were present, Rust treats the g as Foo<'static>, again all sunshine and rainbows.
  • If both group A and group B are present, the fact &'a mut T being invariant over T rejects the code, just like the line in foo should be commented out.
    • But then the compiler message seems a bit misleading here...?

Does such statement, i.e. Rust variance type coersion happens also at local variable definition site besides function call site, make any sense? If so, are there some materials around this aspect of Rust? Or maybe I missed something s.t. the statement is completely off and things just don't work like this?

1 post - 1 participant

Read full topic

🏷️ rust_feed