Questions about lifetimes and borrow checker in the Builder pattern

My goal is to build a complex object with complex multi-step initialization logic. To accomplish this I decided to separate initialization and construction logic(a “builder” pattern). I faced the issue when I introduced field c to the end-product struct that requires lifetime specification:

use std::sync::Arc;

struct Builder {
    a: usize
}

struct Product<'c> {
    a: usize,
    b: Arc<usize>,
    c: Arc<Container<'c>>
}

struct Container<'z> {
    z: Option<&'z usize>
}

impl Builder {
    fn build<'c>(self) -> Product<'c> {
        Product {
            a: self.a,
            b: self.compute_b(),
            c: self.compute_c()
        }
    }
    
    fn compute_b(&self) -> Arc<usize> {
        Arc::new(self.a)
    }
    
    fn compute_c(&self) -> Arc<Container> {
        Arc::new(Container { z: None })
    }
}

fn main() {
    let b = Builder {
        a: 100
    };
    
    let p = b.build();
    
    println!("a: {}, b: {}", p.a, p.b);
}

(Playground 1)

The error is:

error[E0515]: cannot return value referencing function parameter `self`
  --> src/main.rs:19:9
   |
19 | /         Product {
20 | |             a: self.a,
21 | |             b: self.compute_b(),
22 | |             c: self.compute_c()
   | |                ---- `self` is borrowed here
23 | |         }
   | |_________^ returns a value referencing data owned by the current function

The error description seems clear to me, but I don’t understand why the checker complains about c: self.compute_c() only? Why not b: self.compute_b(),? Both do pretty much the same job. Moreover, Builder::compute_c() doesn’t even use any values from the Builder, whereas Builder::compute_b() does. And if I remove c field from the Product struct it works perfectly fine. See: Playground 2.

Why is it so and how to fix it? Maybe I made a mistake in lifetime specification?

Thanks in advance!

fn compute_c(&self) -> Arc<Container> {
    Arc::new(Container { z: None })
} 

Means the same as

fn compute_c(&self) -> Arc<Container<'_>> {
    Arc::new(Container { z: None })
} 

Which ties the lifetime of Container to self. But this isn’t necessary to tie it to self. So you can change it to either

fn compute_c(&self) -> Arc<Container<'static>> {
    Arc::new(Container { z: None })
} 

Or

fn compute_c<'a>(&self) -> Arc<Container<'a>> {
    Arc::new(Container { z: None })
} 

To make it work. To read more about how '_ works, look up lifetime elision.

1 Like

Thanks a lot! It works this way, but I faced more “advanced” issue when I try to construct something from the Builder to be set to the Product: Playground 3

error[E0495]: cannot infer an appropriate lifetime for borrow expression due to conflicting requirements
  --> src/main.rs:31:38
   |
31 |         Arc::new(Container { z: Some(&self.a) })
   |                                      ^^^^^^^
   |
note: first, the lifetime cannot outlive the anonymous lifetime #1 defined on the method body at 30:5...
  --> src/main.rs:30:5
   |
30 | /     fn compute_c<'a>(&self) -> Arc<Container<'a>> {
31 | |         Arc::new(Container { z: Some(&self.a) })
32 | |     }
   | |_____^
note: ...so that reference does not outlive borrowed content
  --> src/main.rs:31:38
   |
31 |         Arc::new(Container { z: Some(&self.a) })
   |                                      ^^^^^^^
note: but, the lifetime must be valid for the lifetime 'a as defined on the method body at 30:18...
  --> src/main.rs:30:18
   |
30 |     fn compute_c<'a>(&self) -> Arc<Container<'a>> {
   |                  ^^
   = note: ...so that the expression is assignable:
           expected std::sync::Arc<Container<'a>>
              found std::sync::Arc<Container<'_>>

Why did Rust introduced another anon lifetime here?

No lifetimes can possibly make this code compile, because it creates a dangling pointer. In build, you attempt to return an object that borrows self.a (the borrow created in compute_c), but self is destroyed at the end of the function.

2 Likes

ok. That makes sense.

The more general question then is how to transfer ownership of some stuff from Builder to Product, and then additionally initialize another thing that references of moved values? I mean I can’t(or can I?) instantiate the Product struct in steps. Sorry for newbie questions :slight_smile: Maybe the entire approach is not a Rust-way. I will be appreciated on any advice on how to do it more idiomatic.

Self referential types are very hard to do in Rust, so try and avoid them.

To clarify a bit, the reason why self-referential types are hard in Rust is that moving an object invalidates its inner self-references (because they still point to the old memory address of the object), and people care very strongly about the “move is just a memcpy” property so it’s unlikely that a language hook for customizing moves like C++'s move constructor will ever appear.

The remaining solutions are to either 1/not use Rust references/pointers but other constructs that are unaffected by moves (such as offsets) in self-referential types, or 2/make self-referential objects immoveable, forcing them to remain at the same memory address for their entire lifetime.

The core Rust language does not support either solution natively yet, but various library-based solutions exist (rental, owning-ref, std::pin::Pin…). As usual when attempting to implement core language mechanics as libraries, however, ergonomics suffer.

3 Likes