std::vector vs; // as before
… // add elements to vs
vs.emplace( vs.begin(), "xyzzy"); // add "xyzzy" to
// beginning of vs
For this code, few implementations will construct the added std::string
into the memory occupied by vs[0]
. Instead, they'll move-assign the value into place. But move assignment requires an object to move from, and that means that a temporary object will need to be created to be the source of the move. Because the primary advantage of emplacement over insertion is that temporary objects are neither created nor destroyed, when the value being added is put into the container via assignment, emplacement's edge tends to disappear.
Alas, whether adding a value to a container is accomplished by construction or assignment is generally up to the implementer. But, again, heuristics can help.
Node-based containers virtually always use construction to add new values, and most standard containers are node-based. The only ones that aren't are std::vector
, std::deque
, and std::string
. ( std::array
isn't, either, but it doesn't support insertion or emplacement, so it's not relevant here.) Within the non-node-based containers, you can rely on emplace_back
to use construction instead of assignment to get a new value into place, and for std::deque
, the same is true of emplace_front
.
• The argument type(s) being passed differ from the type held by the container. Again, emplacement's advantage over insertion generally stems from the fact that its interface doesn't require creation and destruction of a temporary object when the argument(s) passed are of a type other than that held by the container. When an object of type T
is to be added to a container
, there's no reason to expect emplacement to run faster than insertion, because no temporary needs to be created to satisfy the insertion interface.
• The container is unlikely to reject the new value as a duplicate. This means that the container either permits duplicates or that most of the values you add will be unique. The reason this matters is that in order to detect whether a value is already in the container, emplacement implementations typically create a node with the new value so that they can compare the value of this node with existing container nodes. If the value to be added isn't in the container, the node is linked in. However, if the value is already present, the emplacement is aborted and the node is destroyed, meaning that the cost of its construction and destruction was wasted. Such nodes are created for emplacement functions more often than for insertion functions.
The following calls from earlier in this Item satisfy all the criteria above. They also run faster than the corresponding calls to push_back
.
vs.emplace_back("xyzzy"); // construct new value at end of
// container; don't pass the type in
// container; don't use container
// rejecting duplicates
vs.emplace_back(50, 'x'); // ditto
When deciding whether to use emplacement functions, two other issues are worth keeping in mind. The first regards resource management. Suppose you have a container of std::shared_ptr
s,
std::list> ptrs;
and you want to add a std::shared_ptr
that should be released via a custom deleter (see Item 19). Item 21explains that you should use std::make_shared
to create std::shared_ptr
s whenever you can, but it also concedes that there are situations where you can't. One such situation is when you want to specify a custom deleter. In that case, you must use new
directly to get the raw pointer to be managed by the std::shared_ptr
.
If the custom deleter is this function,
void killWidget(Widget* pWidget);
the code using an insertion function could look like this:
ptrs. push_back(std::shared_ptr(new Widget, killWidget));
It could also look like this, though the meaning would be the same:
ptrs. push_back({ new Widget, killWidget });
Either way, a temporary std::shared_ptr
would be constructed before calling push_back
. push_back
's parameter is a reference to a std::shared_ptr
, so there has to be a std::shared_ptr
for this parameter to refer to.
The creation of the temporary std::shared_ptr
is what emplace_back
would avoid, but in this case, that temporary is worth far more than it costs. Consider the following potential sequence of events:
1. In either call above, a temporary std::shared_ptr
object is constructed to hold the raw pointer resulting from “ new Widget
”. Call this object temp
.
2. push_back
takes temp
by reference. During allocation of a list node to hold a copy of temp
, an out-of-memory exception gets thrown.
3. As the exception propagates out of push_back
, temp
is destroyed. Being the sole std::shared_ptr
referring to the Widget
it's managing, it automatically releases that Widget
, in this case by calling killWidget
.
Even though an exception occurred, nothing leaks: the Widget
created via “ new Widget
” in the call to push_back
is released in the destructor of the std::shared_ptr
that was created to manage it ( temp
). Life is good.
Now consider what happens if emplace_back
is called instead of push_back
:
ptrs. emplace_back(new Widget, killWidget);
1. The raw pointer resulting from “ new Widget
” is perfect-forwarded to the point inside emplace_back
where a list node is to be allocated. That allocation fails, and an out-of-memory exception is thrown.
2. As the exception propagates out of emplace_back
, the raw pointer that was the only way to get at the Widget
on the heap is lost. That Widget
(and any resources it owns) is leaked.
In this scenario, life is not good, and the fault doesn't lie with std::shared_ptr
. The same kind of problem can arise through the use of std::unique_ptr
with a custom deleter. Fundamentally, the effectiveness of resource-managing classes like std::shared_ptr
and std::unique_ptr
is predicated on resources (such as raw pointers from new
) being immediately passed to constructors for resource-managing objects. The fact that functions like std::make_shared
and std::make_unique
automate this is one of the reasons they're so important.
In calls to the insertion functions of containers holding resource-managing objects (e.g., std::list>
), the functions' parameter types generally ensure that nothing gets between acquisition of a resource (e.g., use of new
) and construction of the object managing the resource. In the emplacement functions, perfect-forwarding defers the creation of the resource-managing objects until they can be constructed in the container's memory, and that opens a window during which exceptions can lead to resource leaks. All standard containers are susceptible to this problem. When working with containers of resource-managing objects, you must take care to ensure that if you choose an emplacement function over its insertion counterpart, you're not paying for improved code efficiency with diminished exception safety.
Читать дальше