Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

`if` branch with owned and borrowed value without let binding

Tags:

rust

ownership

I have this if statement where both branches have to return a &HashMap<..>. In one of the branches though I have an owned HashMap and the other one I'm accessing one from a reference to some struct. Currently, I'm being forced to define an unbound let variable outside the if block to hold the owned map from the first branch (otherwise it would be freed at the end of the if block and the reference would be invalid).

struct Descriptor {
    env: HashMap<String, String>
}

fn merge_env(_a: &HashMap<..>, _b: &HashMap<..>) -> HashMap<String, String> {
    todo!()
}

fn example(d1: &Descriptor, d2: &Descriptor, feature_on: bool) {
    // Can I somehow avoid having to declare this?
    let holder;
    let env = if feature_on {
        holder = merge_env(&d1.env, &d2.env);
        &holder
    } else {
        &d1.env
    };
    read_env(&env);
}

fn read_env(_env: &HashMap<String, String>) { todo!() }

Playground

Is there way to do this avoiding the unbound let? Maybe some wrapper that can hold both an owned and borrowed value? Also, is the current way idiomatic?

like image 479
ᴘᴀɴᴀʏɪᴏᴛɪs Avatar asked Dec 30 '22 13:12

ᴘᴀɴᴀʏɪᴏᴛɪs


2 Answers

Variables have scope in rust. Once their scope ends, they are de-allocated. Thus if you declare your holder inside the if like that:

if feature_on {
        let holder = merge_env(&d1.env, &d2.env);
        &holder
} 

// holder has already been dropped at that point. It would be "use after free" error to use it here

It would be impossible to use it outside the if block, because it would have been dropped at that point. Thus you must make sure that the owned object lives long enough.

One of the possible solutions is what you have already done - by moving the owning variable before the if you have guaranteed that the holder instance will not be dealocated , thus avoiding the use after free error.

The other solution is to use the enum std::borrow::Cow, which has two variants - Cow::Borrowed - for holding references and Cow::Owned for holding the actual instance. This perfectly fits your use case:

fn example(d1: &Descriptor, d2: &Descriptor, feature_on: bool) {
    let env =  if feature_on {
        Cow::Owned(merge_env(&d1.env, &d2.env))
    } else {
        Cow::Borrowed(&d1.env)
    };
    read_env(&env);
}
like image 51
Svetlin Zarev Avatar answered Feb 25 '23 06:02

Svetlin Zarev


You can refactor holder to be an Option and have the None case be what is currently your else-branch. Then, a second variable can be a ref into that Option or to an argument (type annotations are not needed, just for explanation):

fn example(d1: &Descriptor, d2: &Descriptor, feature_on: bool) {
    let holder: Option<HashMap<_, _>> = feature_on.then(|| merge_env(&d1.env, &d2.env));
    let env: &HashMap<_, _> = holder.as_ref().unwrap_or(&d1.env);
    read_env(env);
}

If you really want to get rid of the temporary variable, you can inline all of this:

fn example(d1: &Descriptor, d2: &Descriptor, feature_on: bool) {
    read_env(
        feature_on
            .then(|| merge_env(&d1.env, &d2.env))
            .as_ref()
            .unwrap_or(&d1.env),
    );
}

Considering the "is this idiomatic": I would say, that your current code is absolutely fine too.

like image 25
Niklas Mohrin Avatar answered Feb 25 '23 07:02

Niklas Mohrin