4 ms·
So emplace_back() exists to forward the contructor's arguments to the place where the object can be constructed in-place while push_back() takes an already cons
by Joker_vD 2y ago
So emplace_back() exists to forward the contructor's arguments to the place where the object can be constructed in-place while push_back() takes an already constructed object and (hopefully) moves it to where its needed.
You know, maybe forwarding the arguments is the wrong direction. Maybe there should have been the dual mechanism of back-propagating the ultimate destination of the object all the way back to its constructor instead.
- immibis 2y agoThat's sort of what happens with named return value optimization. You write this: bar foo() {bar baz; ...; return baz;} bar qux = foo(); 'baz' is constructed directly in the space allocated for 'qux'. Implementation notes: the complex value is never returned but rather the caller passes the address of a space where the return value should be constructed. Inside the function, the compiler notes that baz is returned and allocates it in the return value space. This optimization is guaranteed to occur. Compilers which don't do this are nonconforming.
- Joker_vD 2y ago> This optimization is guaranteed to occur. I vaguely remember that there are some obscure corner cases when it's won't occur but I can't recall them since I haven't written C++ since 2016.
- Arnavion 2y ago>This optimization is guaranteed to occur. Compilers which don't do this are nonconforming. NRVO is not guaranteed to occur, only unnamed RVO (`bar foo() { return bar(...) ; }`) is.
- rtpg 2y agoI really appreciate Rust's copy/clone semantics when reading this. The "hopefully" is so worrying. Production code in Rust is littered with imperformant `.clone()`s but at least I can see where they're happening to ponder a better way.
- Arnavion 2y agoActually, Rust would also really like an `emplace_back`, because it does have the issue that `.push_back(Widget::new(foo, bar, baz))` can end up creating a Widget local in the caller and then moving it into the Vec allocation. It doesn't happen as much with optimizations enabled, but it does happen in debug builds. You might say "Big deal, it's just a tiny loss of performance to create a value and then copy it into its final place. Unlike C++ this is guaranteed to only do a copy of bytes, no complex code like a copy ctor. Who cares, especially if it's only noticeable in debug builds?" But it's not just a problem of performance. If it's `Box::new([0_u8; 10 * 1024 * 1024])`, then that 10 MiB array created in the caller's stack can end up blowing the caller's stack. Rust did actually try to add emplace style APIs and a dedicated operator. It would've looked like `vec.place_back() <- Widget::new();` and it would've guaranteed that the generated code did not create a copy in the caller frame. It was never stabilized and instead eventually removed, because it was still not reliable enough to provide that guarantee after all. https://github.com/rust-lang/rfcs/blob/master/text/1228-placement-left-arrow.md https://github.com/rust-lang/rfcs/blob/master/text/1228-plac... https://doc.rust-lang.org/1.26.0/std/vec/struct.Vec.html#method.place_back https://doc.rust-lang.org/1.26.0/std/vec/struct.Vec.html#met... https://github.com/rust-lang/rust/issues/27779#issuecomment-378416911 https://github.com/rust-lang/rust/issues/27779#issuecomment-...
- tialaramex 2y agoThe Box::new_zeroed family lets us sidestep this. I thought it was stabilised, but looks like not yet Box::<u8>::new_zeroed_slice(10 * 1024 * 1024) says we want 10MiB of zero bytes as a MaybeUninit inside a box, we can then (since these are just bytes in our example) assume_init() since that's valid for our type although in the real world probably we'd actually store some actual data in the memory we've allocated - but it doesn't go on the stack. If we're overwriting it all anyway there's also an adjacent set of uninit functions to skip the zero step, although of course the OS might be zeroing the page anyway.
- Arnavion 2y agoRight, in the absence of placement-exprs, the alternatives are based around MaybeUninit. Box::new_zeroed() for the "boxed zero array" case, Box::new_uninit() + MaybeUninit::write() + Box::assume_init() for the "boxed arbitrary large value case", Vec::reserve() + Vec::spare_capacity_mut() + MaybeUninit::write() + Vec::set_len() for the "append to Vec" case, etc.
- tylerhou 2y agoRe: the last paragraph, C++ has temporary materialization — space for temporary objects is not actually “allocated” until the object needs storage (commonly when it binds to a reference). The problem is that push_back takes by rvalue reference, which forces a materialization of the temporary. I don’t see a way around forcing materialization because taking a reference requires some uniform representation for the reference — the temporary could have been constructed by any one of its possibly many constructors. Forwarding arguments makes the parameters explicit, and the language already has support for this, so I don’t see a huge need for adding more magic.
- _huayra_ 2y agoIs this true with respect to the ABI? I get how a temporary is blown away at the end of a statement with a `;` if it wasn't bound to a reference that extends its lifetime, but wouldn't the code need to allocate a return slot regardless? What I'm thinking of would be ```c++ struct MyBigClass { /* lots of members */ }; MyBigClass makeABigOne(); auto main() -> int { // We still need to construct a return slot here, even though we don't use the value, right? makeABigOne(); } ``` Perhaps this is tangential, but I'm wondering if maybe I'm missing a subtly based on what you mean by "allocated" (in the abstract machine, or in the ABI).
- tylerhou 2y agoApparently temporary materialization also occurs when “when a prvalue appears as a discarded-value expression.” https://en.cppreference.com/w/cpp/language/implicit_conversion#Temporary_materialization https://en.cppreference.com/w/cpp/language/implicit_conversi... So yes, storage for the (to-be discarded) object must be allocated, and the object is constructed into that storage. I don’t know enough about the ABI to comment about the last point.
- phaedrus 2y agoC++ already has (named) return value optimization; I think the behavior you describe would happen when passing the result of a function to push_back/emplace_back. https://en.cppreference.com/w/cpp/language/copy_elision https://en.cppreference.com/w/cpp/language/copy_elision