Lifetime in operator overloading


#1

I encountered a strange problem in the program below. If I replace the explicit method call with an equivalent (I believe) overloading operator, as commented in the program, the compiler (1.16.0) complains about lifetime problems. Why?

use std::ops::Add;

struct Foo {
    x: i32
}

impl<'a> Add for &'a Foo {
    type Output = Foo;
    fn add(self, rhs: Self) -> Foo {
        Foo{ x: self.x + rhs.x }
    }
}

impl<'a> Add<Foo> for &'a Foo {
    type Output = Foo;
    fn add(self, rhs: Foo) -> Foo {
        self.add(&rhs)
        // If we replace the above line with the next line,
        // it won't compile. Why? They look identical.
        // self + &rhs
    }
}

fn main() {
    assert_eq!((&Foo{ x: 1 } + &Foo{ x: 2 }).x, 3);
}

#2

With two lifetimes in the first impl using operator in the second one works.

impl<'a, 'b> Add<&'b Foo> for &'a Foo {
    type Output = Foo;
    fn add(self, rhs: &Foo) -> Foo {
        Foo{ x: self.x + rhs.x }
    }
}

#3

Thanks! But with only one lifetime, why does the first one work, as in my original program? How are these two lines different?

self.add(&rhs)
self + &rhs

#4

This is a known bug with lifetimes and operator overloading (Issue #32008). I’m not too familiar with how lifetimes interact with operator overloading and inference but the below is my understanding (someone please correct me if I’m wrong):

Your first implementation expands to impl<'a> Add<&'a Foo> for &'a Foo so when your second implementation consumes Foo and creates a new reference, it doesn’t have the 'a lifetime since it’s in a difference scope.

Now, normally that’s not a problem because when you call self.add(&Foo), Rust can perform an implicit reborrow that casts the reference to &'a Foo (as long as it’s safe) but the compiler won’t do that in operator overloading situations (or can’t, due to ambiguity in type inference - I’m not sure). The lifetime error you are seeing has probably changed in the year since this issue was filed but the root cause is still the same. Operator overloading isn’t a simple desugaring process of a + b to a.add(b) so type inference, especially with lifetimes, can behave unexpectedly.

With two lifetimes in the implementation, the ambiguity disappears since Rust can just generically plug any 'a or 'b, regardless of their relationship to each other and you don’t have to rely on reborrowing to satisfy the single lifetime constraint.