Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RFC: core::mem::replace_with for temporarily moving out of ownership. #1736

Closed
wants to merge 2 commits into from
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 115 additions & 0 deletions text/0000-mem-replace-with.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
- Feature Name: mem-replace-with
- Start Date: 2016-09-01
- RFC PR: (leave this empty)
- Rust Issue: (leave this empty)

# Summary
[summary]: #summary

Add a new function to `core::mem` (and thus `std::mem`), `replace_with`, which
invokes a closure that takes temporary ownership over some value behind a
mutable reference.

This is similar to the functions provided by [the take_mut crate](https://crates.io/crates/take_mut).

# Motivation
[motivation]: #motivation

A common pattern, especially for low-level programmers, is to acquire ownership
of a value behind a reference without immediately giving it a replacement value.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you include a simple example of this pattern and the problem it causes?


`core::mem::replace_with` allows such thing by generalizing
`core::mem::replace`, to allow the replacement value being generated by a
closure and even depend on the old value.

Unfortunately, there are no safe implementation in the standard library,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/are/is/ to match "implementation". Or alternatively, "the standard library doesn't provide any safe implementation"

leading some Rust users to either reimplement a safe interface for such a
feature (often flawed due to unwinding, see below) or use things like
`ptr::read`.

This should standardize the ecosystem with respect to that.

# Detailed design
[design]: #detailed-design

A complete implementation can be found [here](https://github.com/rust-lang/rust/pull/36186). This implementation is pretty simple, with only one trick which is crucial to safety: handling unwinding.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we just include the code here? The implementation is short, and including makes the RFC self-contained.


Without handling unwinding, the closure could unwind and invoke destructors on
the invalid value. Thus we use "exit guards" which are placed in the stack
frame (as a variable) and holds a destructor exiting the program.

This behavior is simply specified to match the one of panicking inside
unwinding destructors. This has the nice property of allowing error messages in
the libcore implementation.

## Implementation

```rust
/// A guarding type which will abort upon drop.
///
/// This is used for catching unwinding and transforming it into abort.
///
/// The destructor should never be called naturally (use `mem::forget()`), and only when unwinding.
struct ExitGuard;

impl Drop for ExitGuard {
fn drop(&mut self) {
// To avoid unwinding, we abort (we panic, which is equivalent to abort inside an unwinding
// destructor) the program, which ensures that the destructor of the invalidated value
// isn't runned, since this destructor ought to be called only if unwinding happens.
panic!("`replace_with` closure unwinded. For safety reasons, this will \
abort your program. Check the documentation");
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unwinded -> unwound

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also isn't runned

}
}

/// Temporarily takes ownership of a value at a mutable location, and replace it with a new value
/// based on the old one.
///
/// We move out of reference temporarily, to apply a closure, returning a new value, which is then
/// placed at the original value's location.
///
/// # An important note
///
/// The behavior on panic (or to be more precise, unwinding) is specified to match the behavior of
/// panicking inside a destructor, which itself is simply specified to not unwind.
#[inline]
#[unstable(feature = "replace_with", issue = "...")]
pub fn replace_with<T, F>(val: &mut T, replace: F)
where F: FnOnce(T) -> T {
// Guard against unwinding. Note that this is critical to safety, to avoid the value behind the
// reference `val` is not dropped twice during unwinding.
let guard = ExitGuard;

unsafe {
// Take out the value behind the pointer.
let old = ptr::read(val);
// Run the closure.
let new = closure(old);
// Put the result back.
ptr::write(val, new);
}

// Forget the guard, to avoid panicking.
mem::forget(guard);
}
```

# Drawbacks
[drawbacks]: #drawbacks

It expands the core library slightly, and the panic semantics might not be
clear for everyone.

# Alternatives
[alternatives]: #alternatives

1. Specify the unwinding behavior to abort.

2. Use `catch_unwind` and print an error on unwinding (in libstd).

4. Leave it out of the standard library.

# Unresolved questions
[unresolved]: #unresolved-questions

Are there currently any crates relying on invariants that this will break?