If you have &mut
, there is only one viewer of the target state, so there cannot be a need of the swap to be atomic, because nobody else can see the swap happening anyway.
You cannot do this, because this is unsound. You have to access the "reset on end of scope"d object from the wrapper. This kind of subtle problems with your design is the reason why Rust doesn't like it and forces you to use a different design. Here's the proof upthread, but to summarize:
- If you can still access the original binding, you still have a longer lifetime
- And that means you can spawn a thread that respects that longer lifetime and holds the
&mut
borrow and uses it
- And that means that your reset on drop aliases the still running other thread's borrow.
This is super important. Your C++ design is never provably sound, so it cannot be expressed in safe Rust. "Only accesses the pointer at drop time" is not actually enough to make the pattern sound in the face of (scoped) concurrency.
Phrased in Rust terms, that means you cannot have a borrow lifetime that is valid across the point at which you do the mutation. Plus, "just make the compiler smarter" isn't a viable solution in this case, because borrowing rules are already the most complicated thing in Rust that many developers struggle with, so making it more complicated will just make the problem worse. On top of that, we barely have a formal understanding of why Rust's lifetime rules are sound, let alone any proposed extensions to the rules. And finally, we have a way of telling the compiler that we know what we're doing: using raw pointers. You just have to only use raw pointers, so that the useful qualities of references still stay true.
Thankfully, that's not a worry in Rust, because there isn't a concept of a null
missing value.
That's not too difficult to support properly on top of my TemporarilySet
:
impl<'a, Type, Field: 'a, Accessor> TemporarilySet<'a, Type, Field, Accessor>
where
Accessor: for<'b> FnMut(&'b mut Type) -> &mut Field,
{
fn forget(self) {
let mut this = std::mem::ManuallyDrop::new(self);
unsafe {
std::ptr::drop_in_place(&mut this.accessor);
std::ptr::drop_in_place(&mut this.reset_to);
}
}
}
The only reason this isn't just a std::mem::forget
is to make sure that we clean up our members properly. scopeguard
also offers a defusing into_inner
function for their guard.
This is where scopeguard is typically used.
scopeguard::defer! { pwndTar.SuppressPaint(false) }
pwndTar.SuppressPaint(true);
This is a lot clearer locally I'd say than having to pull up the docs of TWndPaintJanitor
to know what state exactly it manages.
I made a different version that's more evil, hiding the wrapper entirely.
#[derive(Debug, Default)]
struct HeavyStruct {
is_critical_section: bool,
// other fields
}
fn main() {
let mut resource = HeavyStruct::default();
dbg!(&resource);
{
temporarily_set!(&mut resource, resource.is_critical_section, true);
dbg!(&resource); // note: this is actually a different resource binding
}
dbg!(&resource);
}