Thanks a lot for the clarification. 
I suppose (again) that UB is "undefined behaviour", and not "unsafe block", since it's ambiguous here.
If it's "undefined behaviour", it indeed clarifies the misunderstanding. But I'd rather call it a side-effect, because the behaviour seems well defined. The two blocks aren't actually "alive" at the same time in the same sense as asynchronous code; they're executed one after the other, but they reference the same variable simultaneously, which is bad. As a result, COUNT
is increased twice, and since it's referenced in each structure and not copied, that's the value displayed at the end: 2.
That's what I said in my previous post: the result is predictable and logical, unless the compiler makes the wrong assumption that it can optimize the code in some way; for example, by using two CPU registers caching the value COUNT, ultimately displaying a 1 or a 2 depending on the level of optimization, the compiler version, or who knows what else (I don't think it's the case).
Here, the programmer used unsafe blocks without doing a proper check and unfortunately doesn't benefit from the borrow checker. The same error could happen by duplicating &mut
of any non-static variable in unsafe blocks (Miri will also detect that). For example, I had to do that when dealing with tree structures and a mutable iterator, but I knew that it was OK per design and double-checked with Miri. Then I checked again and made sure to add the safety comments.
If you replace the static variable by UnsafeCell
(SyncUnsafeCell
is currently unstable, and I don't think it would change anything in this single-threaded code), you get the same error:
static mut COUNT: UnsafeCell<u32> = UnsafeCell::new(0);
...
fn main() {
let mut foo = new_foo(unsafe { COUNT.get_mut() });
let mut bar = new_bar(unsafe { COUNT.get_mut() });
...
=> Foo { thing: "new_foo", counter: 2 } Bar { thing: "new_bar", counter: 2 }
So, regarding what's obvious or not, or instructive or not, I think it's subjective and we'll have to agree to disagree. If C/C++ developers internalize a recipe for using static variables, they'll do the same with UnsafeCell
, feel safe, and commit the same error. Not that I really think a C/C++ developer will be any different than a Rust developer here: both are familiar with pointers, and reading a code like that should switch on a big caution light in their mind. Changing a variable in uppercase should be a pretty obvious clue, too. Since the "solution" here doesn't guarantee safety, it's best to avoid making it look so, and rather count on programmers to keep on their toes around unsafe code as usual.
Maybe that makes me a wild, evil programmer, but I believe that the sane reaction would have been (a) examining how the static variable is used and (b) taking the appropriate decision:
- is it only assigned once (from the program arguments, for ex)? => nothing special to do, but check that it's only used after being assigned.
- is it assigned several times / complicated (like in the code you kindly provided)? => it seems more hairy, let's use
RefCell
.
- is it a multithread or are there coroutines? => ... (maybe that's the use-case where the compiler should incite the use of proper synchronization)