Join GitHub today
GitHub is home to over 31 million developers working together to host and review code, manage projects, and build software together.
Sign upRFC: `core::mem::replace_with` for temporarily moving out of ownership. #1736
Conversation
ticki
referenced this pull request
Sep 1, 2016
Closed
Implement `replace_with`, a function similar to the one provided by the `take_mut` crate. #36186
This comment has been minimized.
This comment has been minimized.
|
cc. @Sgeo @makoConstruct |
BurntSushi
added
the
T-libs
label
Sep 1, 2016
ticki
referenced this pull request
Sep 1, 2016
Open
Include `fn take_mut<T, F: FnOnce(T) -> T>(&mut T, F)` in libstd #1508
kennytm
reviewed
Sep 1, 2016
| # 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. |
This comment has been minimized.
This comment has been minimized.
kennytm
Sep 1, 2016
Member
Should we just include the code here? The implementation is short, and including makes the RFC self-contained.
This comment has been minimized.
This comment has been minimized.
|
So, from what I'm reading, panicking inside of the closure results in implementation defined behavior? That seems... bad. I'd rather just define it now to, well, abort is my preference. |
This comment has been minimized.
This comment has been minimized.
|
@ubsan Undefined != Unspecified. Check the comments from the code. Unwinding will make it abort, but the behavior is unspecified, and might thus change in the future (just like the dropping order of a vector's elements and arithmetic overflows). Edit: @usban edited his comment. Let me just reply to the new edition of it: Well, the point is to keep all possibilities open, and frankly, there might be even more in the future. One thing we could define is that it mirrors the behavior of destructors in destructors. |
This comment has been minimized.
This comment has been minimized.
|
Speaking of keeping all possibilities open, is there any reason this couldn't be implemented to simply mem::forget the old value to avoid double-drop without aborting or catching the panic? I'm not sure that's what I'd prefer, but it's what I intuitively expected before I saw the abort() in your PR. |
This comment has been minimized.
This comment has been minimized.
|
@lxrec |
This comment has been minimized.
This comment has been minimized.
|
@ticki 1) not a male. 2) I am pretty sure I wrote "implementation defined behavior" in my original comment. I don't like implementation defined behavior. And goddamnit, check the gender of the people you're talking about. This is getting ridiculous. When you say "unspecified", that means "undefined behavior", by the way. A compiler could do anything, unless you actually give it options of what to do. What, exactly, in your RFC is stopping a compiler from writing to null in the case of a panic? |
This comment has been minimized.
This comment has been minimized.
The fact that it is safe denotes that it is not breaking invariants (and thus leading to UB). |
This comment has been minimized.
This comment has been minimized.
|
@ticki I've seen a lot of broken safe code :3 My opinion is, obviously, that we should define it. However, I think if we don't, we should give specific options to take, because that is what unspecified is to me. An implementation chooses one of a specific set of options, and doesn't have to document it (unlike implementation defined, where an implementation should document it). |
This comment has been minimized.
This comment has been minimized.
Well, safe code (while it could be broken) can always be assumed to work right. Safe code shouldn't break any invariants.
There are already a lot of unspecified things in standard library. Vec drop order comes to my mind. Overflows is a case too. |
This comment has been minimized.
This comment has been minimized.
|
@ticki Overflows are defined to be two's complement for signed integers, and reset to zero for unsigned integers OR panic. Vec drop order (as well as all drop orders, except for scope) are unspecified, but they are given (implicit) options to choose from; forwards or backwards. |
This comment has been minimized.
This comment has been minimized.
Please link. RFC 560 says otherwise.
Well, not really. In theory, any order could be used. In the end, it is unspecified, and having panics unspecified here is neither unsafe nor dangerous. The type system still expresses an important invariant, i.e. that panicking is safe. Whether it aborts, overwrites the value, or anything else is certainly important as well, but the price paid for specifying it is stagnation. |
This comment has been minimized.
This comment has been minimized.
|
And, again, unspecified behavior is completely unrelated to undefined behavior. |
This comment has been minimized.
This comment has been minimized.
|
C++17 §1.3.26[defns.unspecified]
Could the RFC just specify some options to choose from (i.e. abort = alternative 1 / catch_unwind = alternative 2 / |
This comment has been minimized.
This comment has been minimized.
|
Eating your laundry is definitely a guarantee. Haven't you read the Rust website? |
This comment has been minimized.
This comment has been minimized.
|
@ticki the version of RFC 560 you linked is a pre-acceptance draft. The accepted version is here and doesn't leave things unspecified. Anyway I would agree that a safe function being documented to do something unspecified does not imply that it could do something unsound. In this case I haven't seen any suggestions as to what the code could do besides aborting, so it might stop the argument if we just defined that (I would like to leave the option open to print something if |
joshtriplett
reviewed
Sep 9, 2016
| [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. |
This comment has been minimized.
This comment has been minimized.
joshtriplett
Sep 9, 2016
Member
Can you include a simple example of this pattern and the problem it causes?
joshtriplett
reviewed
Sep 9, 2016
| `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, |
This comment has been minimized.
This comment has been minimized.
joshtriplett
Sep 9, 2016
Member
s/are/is/ to match "implementation". Or alternatively, "the standard library doesn't provide any safe implementation"
This comment has been minimized.
This comment has been minimized.
This is probably obvious, but it's early in the morning and I've only had 2 cups of coffee (clearly not enough). Can you say more about the connection to the behavior of unwinding in a dtor? (Do you mean specifically double panic? Or just any dtor in a dtor?) |
This comment has been minimized.
This comment has been minimized.
|
I'm generally EDIT: It might be a good idea to be careful with the documentation to appropriately convey the caution that is required here =) |
This comment has been minimized.
This comment has been minimized.
It's a typo. I meant "panic in destructors". |
This comment has been minimized.
This comment has been minimized.
Let me rephrase. Is there some particular semantics for panicing in destructors (or during unwinding, or both) that would make it possible for you to do something other than abort here? |
This comment has been minimized.
This comment has been minimized.
plietar
commented
Sep 15, 2016
|
Could this be specialised such that if |
This comment has been minimized.
This comment has been minimized.
|
@plietar The point is exactly that there is no substituded value. Indeed, you can use simply |
This comment has been minimized.
This comment has been minimized.
bluss
commented
Sep 28, 2016
|
aturon
closed this
Jan 4, 2017
This comment has been minimized.
This comment has been minimized.
|
Just to provide some additional motivation: I stumbled on this RFC just now mostly by accident, and I realized I had previously written code that made this exact mistake (i.e. didn't abort on panics in the constructor). |
This comment has been minimized.
This comment has been minimized.
lostman
commented
Apr 10, 2017
|
I wrote a similar function (ignoring safety concerns) recently when implementing a simple list: enum List<T> {
Nil,
Cons(T, Box<List<T>>)
}In particular I wanted to implement struct IntoIter<T>(List<T>);
impl<T> IntoIterator for List<T> {
type Item = T;
type IntoIter = IntoIter<T>;
fn into_iter(self) -> Self::IntoIter {
IntoIter(self)
}
}My first implementation was: impl<T> Iterator for IntoIter<T> {
type Item = T;
fn next(&mut self) -> Option<T> {
match mem::replace(&mut self.0, List::Nil) {
List::Nil => None,
List::Cons(x, l) => {
mem::replace(&mut self.0, *l);
Some(x)
}
}
}
}Which seems quite bad. First, it requires replacing the list with Using pub fn replace_with<T,R,F>(dest: &mut T, f: F) -> R
where F: Fn(T) -> (R, T)
{
unsafe {
let (r, v) = f(std::ptr::read(&*dest));
std::ptr::write(dest, v);
r
}
}Note that impl<T> Iterator for IntoIter<T> {
type Item = T;
fn next(&mut self) -> Option<T> {
replace_with(&mut self.0, |l| {
match l {
List::Nil => (None, List::Nil),
List::Cons(x, rest) => (Some(x), *rest)
}
})
}
}I'm new to Rust but unless I'm missing something |
Sgeo
referenced this pull request
Jun 29, 2017
Merged
Add take_or_recover() which replaces the value with a recovery value on panics #3
This comment has been minimized.
This comment has been minimized.
bchallenor
commented
Jan 21, 2018
|
Given that this was closed for lack of motivating examples, I'd like to link to this thread where I bumped into this issue. I was also tempted to implement it myself, and didn't consider panics. |
This comment has been minimized.
This comment has been minimized.
bergus
commented
Jun 10, 2018
|
@antrik Regarding the name, how about @briansmith The double- |
This comment has been minimized.
This comment has been minimized.
briansmith
commented
Jun 11, 2018
|
@bergus Yes, I think everybody here understands that. The solution is to make abort-on-panic the one and only panic semantics. Then we'd never need poisoning anywhere, including in |
This comment has been minimized.
This comment has been minimized.
|
To echo @bergus' comment, I recently ran into this with the state machine pattern of transitioning between states by taking something by value, and struct StateA { ... }
struct StateB { ... }
struct StateC { ... }
impl StateA {
// note: takes self by value
fn transition_b(self) -> State B { ... }
}
enum State {
A(StateA),
B(StateB),
C(StateC),
}
struct Foo {
// how do I transition this field between states?
state: State,
} |
This comment has been minimized.
This comment has been minimized.
alecmocatta
commented
Nov 8, 2018
•
|
Just to add to the Here's my crate that resolves the common cases for me: replace_with. fn replace_with<T, D: FnOnce() -> T, F: FnOnce(T) -> T>(dest: &mut T, default: D, f: F);
fn replace_with_or_abort<T, F: FnOnce(T) -> T>(dest: &mut T, f: F);
fn replace_with_or_default<T: Default, F: FnOnce(T) -> T>(dest: &mut T, f: F);Implemented using a destructor rather than |
This comment has been minimized.
This comment has been minimized.
vi
commented
Nov 8, 2018
|
@alecmocatta Competing older crate: https://crates.io/crates/take_mut |
This comment has been minimized.
This comment has been minimized.
alecmocatta
commented
Nov 8, 2018
|
@vi Thanks, it differs from take_mut in a couple of ways:
Both minor but, for me at least, both important. |
This comment has been minimized.
This comment has been minimized.
briansmith
commented
Nov 8, 2018
If/when that closure panics, there would still be a surprising abort, right? |
This comment has been minimized.
This comment has been minimized.
alecmocatta
commented
Nov 8, 2018
|
@briansmith Indeed, though note in that situation the user's code would have to have panicked twice, once in the I look forward to when this whole concept is rendered redundant by the type system allowing temporary moves out of |
This comment has been minimized.
This comment has been minimized.
Sgeo
commented
Nov 9, 2018
|
@alecmocatta That's funny, |
This comment has been minimized.
This comment has been minimized.
|
@alexcameron89 I do not think @briansmith was talking about the Think of something like fn foo(x: &mut Vec<i32>) {
scoped_thread_spawn(|| {
mem::replace_with(x, |vec| { drop(vec); panic!() });
});
println!("{:?}", x);
}With your crate, this does a use-after-free. |
This comment has been minimized.
This comment has been minimized.
alecmocatta
commented
Nov 9, 2018
•
|
@RalfJung "The point is that if I think @briansmith's point is whether aborting on the panic of both For the avoidance of talking at cross-purposes, here's the implementation: struct CatchUnwind<F: FnOnce()>(Option<F>);
impl<F: FnOnce()> Drop for CatchUnwind<F> {
fn drop(&mut self) {
self.0.take().unwrap()();
}
}
fn catch_unwind<F: FnOnce() -> T, T, P: FnOnce()>(f: F, p: P) -> T {
let mut x = CatchUnwind(Some(p));
let t = f();
let _ = x.0.take().unwrap();
mem::forget(x);
t
}
pub fn replace_with<T, D: FnOnce() -> T, F: FnOnce(T) -> T>(dest: &mut T, default: D, f: F) {
unsafe {
let t = ptr::read(dest);
let t = catch_unwind(move || f(t), || ptr::write(dest, default()));
ptr::write(dest, t);
}
}
pub fn replace_with_or_default<T: Default, F: FnOnce(T) -> T>(dest: &mut T, f: F) {
replace_with(dest, T::default, f);
}
pub fn replace_with_or_abort<T, F: FnOnce(T) -> T>(dest: &mut T, f: F) {
replace_with(dest, || process::abort(), f);
}And an example: enum States {
A(String),
B(String),
}
impl States {
fn poll(&mut self) {
replace_with(
self,
|| States::A(String::new()),
|self_| match self_ {
States::A(a) => States::B(a),
States::B(a) => States::A(a),
},
);
}
} |
This comment has been minimized.
This comment has been minimized.
|
@alecmocatta oops I missed that |
This comment has been minimized.
This comment has been minimized.
alecmocatta
commented
Nov 9, 2018
|
@RalfJung No worries I suspected there was a crossed wire somewhere! |
This comment has been minimized.
This comment has been minimized.
briansmith
commented
Nov 10, 2018
I think it's more about naming. We have enum Fallback<D, T> {
Const(T),
AbortOnPanic(FnOnce() -> T)
}
|
This comment has been minimized.
This comment has been minimized.
briansmith
commented
Nov 11, 2018
•
|
In a private codebase, I tried out the API I suggested in my previous comment. Now I realize I was wrong to suggest that Further, I think it would be useful to to be consistent with the naming convention used by functions like pub struct AbortOnPanic<T, D: FnOnce() -> T>(pub D);
impl<T, D: FnOnce() -> T> AbortOnPanic<T, D> {
#[inline]
pub fn abort_on_panic(self) -> T { (self.0)() }
}
pub fn replace_with<T, F: FnOnce(T) -> T>(dest: &mut T, fallback: T, f: F) { ... }
pub fn replace_with_or_else<T, D: FnOnce() -> T, F: FnOnce(T) -> T>(
dest: &mut T,
fallback: AbortOnPanic<T, D>,
f: F
) { ... }This would give the following mapping:
I noticed the proposed implementation uses In general, it isn't clear if/how to make this kind of API work for a type [Edited to fix confusing typo.] |
This comment has been minimized.
This comment has been minimized.
I am fairly sure these are not needed: We are taking the input in a mutable reference. This excludes all aliases. Your concerns about |
This comment has been minimized.
This comment has been minimized.
alecmocatta
commented
Nov 12, 2018
|
@briansmith Your replace_with(dest, Default::default(), f);
// and
replace_with_or_else(dest, Fallback::Const(y), f)are (unless I'm misunderstanding you, please correct me if I am!) a) essentially equivalent and b) equivalent to *dest = f(mem::replace(dest, Default::default()));
// and
*dest = f(mem::replace(dest, y));respectively. (Equivalent, excluding contents of I also agree with @RalfJung that a |
This comment has been minimized.
This comment has been minimized.
|
@alecmocatta I'd say that @briansmith's functions are much easier to read, much clearer in their intent than your constructions involving |
This comment has been minimized.
This comment has been minimized.
briansmith
commented
Nov 12, 2018
@alecmocatta It is true that if there is a default/poison/sentinal value of the type, then you can just use Thanks for pointing out that interior mutability is not an issue with the current API. Regarding atomics, I guess Regarding volatile, I understand the point that we can assume that the value isn't a volatile object because we have a reference to it and it's not safe to have a reference (vs. a pointer) to a volatile object in Rust. However, my point is different; the idea of avoiding storing a sentinel with |
This comment has been minimized.
This comment has been minimized.
It is mandated by the C standard. Whatever that means, but it likely does mean that even obscure hardware architectures support it.
I see. Yes, that makes sense. I'd generally be cautious around using anything even resembling a high-level interface for |
ticki commentedSep 1, 2016
•
edited
This common pattern deserves a place in the standard library.