When Scope Lies: The Wildcard Pattern Drop Footgun in Rust

19 points by tomas


majaha

This surprised me, more than I thought it would after reading the first paragraph.

I was aware that let _ = my_fun(); drops the return value immediately, whereas let _unused = my_fun(); doesn't.

And I thought I knew why, but it turns out I didn't. I'd internalised the idea that "_ is a pattern that matches anything and discards it.", but that's not how it works. Check it out:

struct NonCopy(&'static str);

impl Drop for NonCopy {
    fn drop(&mut self) {
        println!("{}", self.0);
    }
}

struct AlsoNotCopy(NonCopy);

fn main() {
    let a = AlsoNotCopy(NonCopy("apple"));
    println!("0");
    let _ = a;
    let _ = a;// These three don't move out of a, or drop it!
    let _ = a;
    let AlsoNotCopy(_) = a;// Neither does this.
    let _ = AlsoNotCopy(NonCopy("banana"));// This prints "banana" on this line
    // (because it's a temporary)
    println!("1");
    drop(a); // You can still access "a" as a full variable.
    println!("2");
}

This prints: 0 banana 1 apple 2

Rust Playground

_ doesn't move or discard anything at all! The reason that assigning to an underscore sometimes drops is because you're assigning a temporary!

kana

Not to nitpick but the last solution does not work for non-trivial values. The code in the article works because “()” is automatically a “Copy” value in Rust, and the code is copying other_field instead of moving them out of self. [1] To fix it, drop(self.watcher) instead.

To be honest, I'm not quite Rust-fluent and didn't know “let _ = ... ” drops the value for you. But if I do need to depend on dropping order (which might suggest a concurrent environment or runtime-checked borrowing), I will try to be very explicit about every drop: ordering is mostly logical, where memory safety won't help a lot.

[1] Rust playground: Simply making other_field a non-Copy value will break the compilation: “error[E0382]: use of partially moved value: self”.

wrs

If I understand this correctly, the confusion is because of two completely different uses of _:

dpc_pw

Yes, it's a pitfall, but any idiomatic Rust design API should tie what is being protected to the guard's lifetime, so it has to access through the guard, as e.g. in Mutex's guards. This makes this pitfall impossible. "Dettached" guards/watchers are an anti-patter in Rust.

danking

This bit me bad recently when I wanted a tracing span to live for the duration of a block. I wrote what I thought was the natural let _ = span!(…). I spent such a long time looking at logging/tracing configuration before I realized the _ had special semantics.

It’s actually kind of hard to prohibit this behavior. For the specific case of locks there is https://rust-lang.github.io/rust-clippy/master/index.html?search=let_underscore_lock#let_underscore_lock. That doesn’t work for tracing spans (they’re not lock guards). https://doc.rust-lang.org/beta/nightly-rustc/rustc_lint/let_underscore/static.LET_UNDERSCORE_DROP.html will catch the tracing spans but at least in our code base had quite a few spurious cases. IMO, explicit drop is just as readable as let _ = so I prefer we do that.

meithecatte

It feels like everyone ends up discovering this at some point... now, usually someone would've probably added a lint by now, but at least I don't see a way of writing a lint that would pick this up.