Safe Delayed Initialization for Lifetime Extension

A niche programming pattern to satisfy the borrow checker.

Many Rust programmers don't know the exact semantics of let:

A variable in let must be assigned at least once in each control-flow path it is used in (exactly once if it is not declared mut).

This means we don't have to initialize variables if we don't use them:

fn main() {
    let _x: u8; // never assigned a value
}

On its own, this isn't very helpful, but it gets slightly less useless when we combine it with conditional control flow:

use rand;

fn main() {
    let x: u8; 
    
    if rand::random::<bool>() {
        x = rand::random();
        // the borrow checker knows that
        // `x` is always assigned before
        // it is read.
        println!("x is {x}");
    }
}

Ok, still not very useful yet. Where this gets actually useful is when we have a reference that we want to sometimes point to some data we allocated on the heap.

use rand;

fn main() {
    let x_string: String; 
    let mut x = "nothing";
    
    if rand::random::<bool>() {
        let n: u8 = rand::random();
        x_string = n.to_string();
        // because `x_string` is declared
        // in the same scope as `x`,
        // we can do this.
        // if `x_string` was
        // declared within this `if` block,
        // it would not live long enough.
        x = &x_string;
    }
    
    // pretend this is really complicated code
    // that we don't want to repeat.
    println!("x is {x}");
}

Many less-experienced Rust programmers would just use "nothing".to_string() here, always storing x on the heap, but this has the downside of a needless allocation.

Of course, in this example, we could just initialize x_string with a dummy value and make it mut, like let mut x_string = String::new(), but this has a few issues: 1. it implies the string may be mutated several times in-place. 2. if we forget the x_string = line, the compiler won't warn us, and we'll end up printing the empty string. 3. not all types have such a cheap/easy way to create a dummy value like this.

Another alternative would be using MaybeUninit, which may be necessary if your control flow is significantly more complex, but this should be avoided if possible due to the potential to cause Undefined Behavior.

If you're interested in a more real-world example of this pattern, rustdoc uses this in several places, such as in Type::attributes.


#rust #programming


You can follow this blog via its RSS feed or by searching for @binarycat@paper.wf on your Mastodon/ActivityPub instance.