Hacker News new | ask | show | jobs
by hinkley 2446 days ago
Do variables go out of scope after last use or when the function exits? I could see the former evolving into the language if it’s not already the default behavior.

In which case there’s only one situation where I could see this useful, and that’s when you are building a large object to replace an old one.

The semantics of

    foo = buildGiantBoject();
In most languages is that foo exists until reassigned. When the object represents a nontrivial amount of memory, and you don’t have fallback behavior that keeps the old data, then you might see something like

    drop(foo);
    foo = buildGiantBoject();
Most of the rest of the time it’s not worth the hassle.
3 comments

It used to be at the end of the block, which caused all manner of annoyance. So they spent a lot of effort improving the borrow checker, and now it's 'after last use'.

It's not just a matter of memory use. References and mutable references form a sort of compile-time read-write mutex; you can't take a mutable reference without first dropping all other references. See https://stackoverflow.com/questions/50251487/what-are-non-le... for more.

NLL didn't affect when objects get dropped. That would be a breaking change. Things still get dropped at end of block (well, approximately... the exact rule is actual quite complex).
This is incorrect. Values still go out of scope and have their destructors run at the same time.

NLL only affects values without destructors.

I think `#[may_dangle]` is an exception to this, and the standard library puts it on many (most?) container types.
It is not an exception; #[may_dangle] does not change the time drop runs. All it does is promise that drop will not access borrowed data, allowing that data to die before drop: https://doc.rust-lang.org/nightly/nomicon/dropck.html
Not sure how Rust mutexes work but in c++ that wouldn't work. Obvious first example is std::lock_guard which is implemented by locking in constructor and unlocking in destructor. The variable itself never has any "use", it's just created and held alive as a dummy to denote the locking scope.

Now actually this is a quite nasty object with implicit global side effects which you should avoid in the first place, but for the mutex case i don't know of a better option, maybe Rust has a better way to handle this?

Rust’s mutex guard has similar semantics, but acts as a amart pointer to the data in the mutex: if you let the guard drop, you no longer have access to the shared data.
Variables go "out of scope" (in at least one sense) at last use, but are not `Drop`-ed (de-allocated, etc...) until the end of the function. The difference is important because of rust's rule against simultaneous aliasing and mutability. Consider this example:

  fn main() {
    let mut a = 1;
    let b = &mut a;
    *b = 2;
    println!("{}", a); // prints "2"
    // *b = 4; // If this line is uncommented, compile time error.
  }
Because b is a mutable reference to a, this means that a cannot be accessed directly until b goes out of scope. In this sense, b goes out of scope the last time it's used. _However_, AFAIK, b isn't actually de-allocated until the end of the function.

Of course, it doesn't matter in this trivial case, because b is just some bytes in the current stack frame so there's nothing to actually de-allocate. But if b were a complex type that _also_ had some memory to de-allocate, this wouldn't happen until the end of main(). But in this case, b's scope also lasts until the end of main, which is kind of like adding that last line back in...

This can be seen in the following example, where b has an explicit type:

  struct B<'a>(&'a mut i32);
  impl<'a> Drop for B<'a> {
    fn drop(&mut self) {
      // We'd still have a mutable reference to a here...
      // If B owned resources and needed to free them, this is where that would happen
    }
  }
  fn main() {
    let mut a = 1;
    let b = B(&mut a);
    *b.0 = 2;
    std::mem::drop(b); // Comment this line out, get compiler error
    println!("{}", a); // prints "2"
  }

In this example, without the std::mem::drop() line, the implementation for Drop (i.e., B's destructor), B::drop would be implicitly called at the end of the function. But in that case, B::drop() would still have a mutable reference to a, which makes the println call produce a "cannot borrow `a` as immutable because it is also borrowed as mutable" compile time error.

In other words, this "going out of scope at last use" is really about rust's lifetimes system, not memory allocation.

IMHO... this is one of the rough edges in rust's somewhat steep learning curve. Rust's lifetimes rules make the language kind of complicated, though getting memory safety in a systems programming language is worth the trade-off. There's a lot of syntactic sugar that makes things a LOT easier and less verbose in most cases, but the learning curve trade-off for _that_ is that, when you _do_ run into the more complex cases that the compiler can't figure out for you, it's easy to get lost, because there are a few extra puzzle pieces to fit together. Still way better than the foot-gun that is C, though. At least for me... YMMV, obviously.