Pre-RFC: `#[must_bind]`

I’m not 100% sure this is the true reason. At least for if let and while let the temporary lifetime seems to be mainly because these statements are equivalent to match statements, especially if let PAT = EXPR { ... } else { ... } is 100% the same as match EXPR { PAT => { ... }, _ => { ... } }.

And in match statements you need the temporaries because ... well ... you need to keep the value being matched against because there can be multiple arms and special treatment of the last arm would be inconsistent, and you need all the other temporaries because you don’t want all the error messages about temporaries being dropped while borrowed that you would otherwise get when matching against dereferenced mutex or refcell guards etc.

for expressions seem to have a similar reason to match why they need to keep temporaries. The reference presents a translation that keeps the temporaries on purpose.


So now though to my main point ... today I learned:

if EXPR { BLOCK1 } else { BLOCK2 }

is not the same as

match EXPR {
    true => { BLOCK1 }
    false => { BLOCK2 }
}

To expand @dhm’s playground: ⟶ like this, we see that an if statement does drop the temporaries of the condition expression. So a proper translation would be

{
    let cond = EXPR;
    match cond {
        true => { BLOCK1 }
        false => { BLOCK2 }
    }
}

It the rule was about “temporaries are dropped at the end of the statement”, this wouldn’t make any sense. So I guess the actual principle is: Temporaries are dropped at the end of the statement expression for some kinds of expressions where dropping them earlier would cause to many problems.

Now for if expressions, the condition is always an owned bool, so there cannot possibly be any programs not compiling because the condition’s temporaries where dropped earlier.


Honestly, my personal feeling about this is that by pretending there was a consistent and simple rule about temporaries an the end of statements/expressions, we only get less optimization without much actual gain. But it is probably hard to change anything now. The most confusing rule that we have right now, IMHO, is how blocks work. A block { STMTS; EXPR } keeps the temporaries of EXPR in the surrounding expression. This makes { EXPR } similar to a call to an identity function id(EXPR) in that it moves the expression, but it makes it behave different than loop { break EXPR } or (|| EXPR)() or if true { EXPR } else { unreachable!() } or match { _ => { EXPR }} which all drop the temporaries.

It also gives rise to a weird rule that temporaries of a final expression in a function (and probably also a closure) are dropped after the local variables are, in effect this means that

fn blah() {
    STMTS...
    EXPR
}

is different from

fn blah() {
    STMTS...
    return EXPR;
}

(proof)

4 Likes

But you also quite frequently need to keep the temporaries of a let statement! That's why we have temporary lifetime extension to begin with. So why doesn't this match logic apply to let? Because, again, Rust intentionally diverges from the typical let/in to get smaller drop scopes.

An important thing to note here is that if expressions do not have a binding scope, which puts them in an entirely different category than the constructs (functions, if let/while let, for, match, let) I was discussing.

Another important difference around if is that it tends to be used in long chains of else ifs, and while you can view these as grammatically nested, that is not how programmers think about them. Thus these two sub-statement temporary drop scopes in the docs I linked:

Arguably, { ...; EXPR } is just Rust's syntax for the in of let/in- the block provides the binding scope for any contained lets. Viewed from this angle, the let special case is quite similar to the behavior of if- temporaries in the scrutinee are dropped early because both constructs tend to have long-running "bodies."

So if you wanted you could rewrite that section of the reference to group the RHS of a let in with the condition expression of an if. Perhaps you might also rewrite the section on temporary lifetime extension in terms of these shortened temporary drop scopes, and my hypothetical _-drops-temporaries proposal in terms of applying this same shrunken drop scope trick to more constructs.

I know! That’s why my takeaway is that we shouldn’t even try


as can if let statements, that keep the temporaries around throughout all the else ifs and else branches nontheless.

CString::new().as_ptr() continues to bite. #[must_use] seems to be closest to fixing that footgun, so I'm really rooting for this RFC (I've seen other proposals to fixing CString.as_ptr() based on types and borrow checking, but they seemed to be much more complex, and less likely to get implemented)

CString is one place where it's really easy to get use-after-free, and that's below Rust's otherwise high standard. IMHO it would be worth adding #[must_use] even if it was just for CString::new().

1 Like

What is the next step to make an improvement?

Options:

  1. Try to prepare a pull request to rustc adding such attribute. Then follow reviewer's comments;
  2. Write a full RFC (does such a minor warning-only feature deserve a real RFC?). If so, what are points that should mentioned and not forgotten?;
  3. Wait for more feedback here.
1 Like

Shall this idea be officially abandoned or it should advance somehow?

Created a pull request: https://github.com/rust-lang/rust/pull/78715

3 Likes

The pull request seems unlikely to go through.

There is some mention about creating of a project group by starting a thread in Zulip, but I'm not sure whether it is a real call to action or just to make refusal more polite.

I don't know if I should try to do something more about this.

@vi0 I do not think it is a polite refusal. It is not uncommon for the different teams to have broader concerns about how things fit together. I believe they are indeed asking for a design round to make sure that what gets eventually added to the language is of high quality and doesn't preclude potential future work. The PR looks fine and have no insight into what T-lang discussed, so I do not know what they have in mind.

Is it a good idea to try to add a Clippy lint with similar behaviour, but with a hard-coded list of types from various libraries (or just some heuristic based on type name) instead of the #[must_bind] attribute?

imo no. It would severely limit the usefulness of such a lint by design.

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.