Simpler placement new

Since the various “placement new” RFC’s were withdrawn last spring, there doesn’t seem to have been much progress on this issue. Would it be possible to have a much simpler solution that doesn’t require any new syntax or traits?

What if we had a single new library function:

ptr::write_with<T, F>(dst: *mut T, f: F) where F: FnOnce() -> T

which would be equivalent to

ptr::write(dst, f());

except that write_with would be an intrinsic with special compiler support: f() is evaluated using dst for the place context, so f’s result is constructed in-place.

Or in implementation terms: when f()'s result would be created on the stack and memcpy’d to dst, it gets created directly in dst instead.

That’s pretty much it. Library functions would have to be changed to take advantage of this, e.g. to add Vec::push_with and Box::new_with that allocate storage before calling ptr::write(), but it’s all ordinary rust code.

Is this sufficient? It’s a whole lot less machinery than the the various <- / in / &out / Placer proposals had, but I don’t see what problems those solved that this doesn’t.

An even simpler variation

This could work no new functions at all, by making ptr::write(dst, src) an intrinsic with special treatment. If src is a value expression, it’s evaluated in a place context with address dst.

This version might be easier to use and would benefit existing code, but it might be surprising that you can’t write a forwarding wrapper for it without changing its behavior, or write your own functions with the same behavior. vec::push_with etc. would still have to take closures, so ptr::write_with seems more symmetric.

Dynamically sized types

I think this could work with dynamically sized types, though there might be complication around who tells who what the actual size is and what happens if it’s wrong.

Does this actually make enough guarantees?

I’m not sure. The spec doesn’t say what happens when a value expression is evaluated with a value context. I’m interpreting that silence to mean that no move happens (and that no temporary memory location is allocated, so there would be nowhere to move from). But I’m not 100% sure of that. Since temporary allocation and moves are mostly unobservable to programs, maybe the compiler still does it sometimes?

(Edit: credit to @vadimcn who suggested the same thing two years ago; apologies for not noticing that sooner.)

4 Likes

One reason cited for scrapping placement for now was that it’s very difficult to tell when moves might still happen internally in the expression being evaluated, defeating the purpose of using placement. Shoving that expression into a closure does little to solve that problem, and when it does, it’s at the cost of expressiveness, e.g. one example where <- style placement is possibly misleading [1] is when used like place <- fallible_function()?. Closures rule this out because the ? can’t return out of the closure and therefore isn’t useful inside it, but it also means if we manage to make place <- fallible_function()? work without temporaries, then this closure-based approach has no way to handle it.

[1] Naively this has to return a Result into a temporary then match on it and copy out the subset of the bytes that are the payload of Ok, but there are ideas to change the ABI of Result-returning functions that could make this work always.

it’s very difficult to tell when moves might still happen internally in the expression being evaluated

This seems like the perfect being the enemy of the good. Right now there's no solution at all. I can understand not making large language changes when they don't sufficiently solve the problem, but having a single function documented as having slightly special behavior seems pretty harmless, even if authors have to be very careful to get the benefits of it. (In fact, even with no language changes at all, you would get some of the benefits today in Release builds--that's what the boxext crate does; this proposal could be viewed as just making that existing solution more reliable.)

I'm really asking about point #4 in Placement NWBI<- FAQ (New/Box/In/Left Arrow). That point explains why just guaranteeing optimization of Vec::push is inadequate, but it doesn't say why the alternative has to be a language extension. All of the other discussion seems to take as given that a language change is required, and so there's an appropriately high bar for the usability of the result. I think the bar can be a lot lower if it's not a major change, just one new special function.

And the problem you've described (making it more obvious when moves / temp allocations might happen) is orthogonal to the problem of having some way to construct into a designated storage location. I think a solution to that would look more like a Move trait, which (much like Sized) would be auto-added to all trait bounds, but functions could opt-out via ?Move.

Then you could have things like Vec::push_with<T: ?Move, F: FnOnce()->T>(src: *mut T, f: F), which guarantees that push_with doesn't introduce an extra move.

(I think ?Move has been rejected before because there's too little benefit and, but this seems like a catch-22: placement new is wanted, but it needs more predictable guarantees. But the language doesn't have a way to talk about those guarantees, so one would need to be added. But we don't want to add one, because there's not enough need to justify it.)

...

The Result situation is different. Here it's not about obvious vs. subtle behavior; there's a clearly desirable goal (efficient combination of placement new with idiomatic error handling) that's hard to achieve. Again, I think that's a good argument against a major change, but not against a relatively trivial change that doesn't preclude any future proposals.

If Result someday got a special ABI repr (e.g. as suggested in issue 42047 where f() -> Result<T,E> would be compiled as something like f(_0: *mut T, _1: *mut E) -> bool, then you could expose it to container lib authors in a similar way:

ptr::write_result_with<T,E,F>(dst: *mut T, f: F) -> Result<(), E>
    where F: FnOnce() -> Result<T,E>

which would be enough to build e.g. Vec::try_push_with() that could push an Ok result without extra temporary space for a T.

(I left Result out the original post because I didn't want to derail the thread--I'm not advocating for or against anything to do with Result, just observing that using closures for placement new is compatible with future changes along those lines.)

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.