In Item 41 from Effective Modern C++ Scott Meyers mentions this difference and its impact on the efficiency of emplacement with respect to insertion.
I have some doubts about that, but before asking questions about that I need to understand what the difference between these two ways of adding an element is.
Consider the code sample from the book:
std::vector<std::string> vs;
// adding some elements to vs
vs.emplace(v.begin(), "xyzzy");
It's clear that after // adding some elements to vs, it could be that
vs.capacity() == vs.size(), orvs.capacity() > vs.size().
Correspondingly,
vshas to reallocate, and all pre-existing elments invs(those namedvs[0],vs[1], ... before the reallocation takes place) have to be move-constructed to the new memory locations (the new locations ofvs[1],vs[2], ...)vshas tovs.resize(vs.size() + 1)and all pre-existing elements have to be move-assigned to the next index, obviously processing backward, from the last to the first element.
(Obviously I referred to move operations because std::string offers noexcept move operations. If this is not the case, then the scenario above is slightly different and copy operations will be used instead.)
However, going to the crux of my question, right after the code above, the book reads (my italic)
[…] few implementations will construct the added
std::stringinto the memory occupied byvs[0]. Instead, they'll move-assign the value into place. […]
What are the two scenarios, after the room has been done to accomodate the new element?
- If
emplaceadds the element via move assignment, it means it doesv[0] = std::string{strarg};wherestrarg == "xyzzy", right? - But what is the other case of contructing the element occupied by
v[0]? Is it a placementnew? And how would it look like? I guess it should be similar to the chunk of code at the section Placement new here, but I'm not sure how it would look like in this case.
There are many different ways to implement
emplace, and the standard is pretty lax on how implementations must do it.Given a pointer to somewhere allocated by
std::allocator_traits<allocator_type>::allocate, the only way for a vector to construct a new object is withstd::allocator_traits<allocator_type>::construct. For the default allocator, this will call placement new.Now if a reallocation did occur, the obvious way to emplace the new element is to call
allocator_traits::construct(get_allocator(), ptr, std::forward<Args>(args...)). This will be the equivalent ofnew (ptr) std::string("xyzzy"). But note that all other elements were also move constructed to the new buffer viaallocator_traits::construct(get_allocator(), ptr, std::move(old_ptr)).If a reallocation didn't occur, most implementations will just construct an element with
value_type(std::forward<Args>(args...))and move-assign from that (equivalent tov[0] = std::string("xyzzy")). This is what libstdc++ does.Alternatively, instead of move constructing
v[0] = std::string("xyzzy"), the object can be destroyed viaallocator_traits::destroy((&v[0])->~value_type()for the default allocator), and then it can be constructed in place viaallocator_traits::construct. This seems like it would be harder to implement since special care would need to be taken to ensure that the element isn't destroyed twice if the move constructor throws, which is probably why only "few implementations" will do it.As an aside, there is no strong exception guarantee, so
move_if_noexceptdoesn't have to be used and the move constructor may always be called, even if the move constructor is notnoexcept.