Reducing assignment of temporary object to in-place construction

707 views Asked by At

Using std::list supporting move semantics as an example.

std::list<std::string> X;
... //X is used in various ways
X=std::list<std::string>({"foo","bar","dead","beef"});

The most straightforward way for compiler to do the assignment since C++11 is:

  1. destroy X
  2. construct std::list
  3. move std::list to X

Now, compiler isn't allowed to do following instead:

  1. destroy X
  2. contruct std::list in-place

because while this obviously saves another memcpy it eliminates assignment. What is the convenient way of making second behaviour possible and usable? Is it planned in future versions of C++?

My guess is that C++ still does not offer that except with writing:

X.~X();
new(&X) std::list<std::string>({"foo","bar","dead","beef"});

Am I right?

4

There are 4 answers

4
Ggouvine On BEST ANSWER

You can actually do it by defining operator= to take an initializer list. For std::list, just call

    X = {"foo","bar","dead","beef"}.

In your case, what was happening is actually:

  1. Construct a temporary
  2. Call move assignment operator on X with the temporary

On most objects, such as std::list, this won't actually be expensive compared to simply constructing an object.

However, it still incurs additional allocations for the internal storage of the second std::list, which could be avoided: we could reuse the internal storage already allocated for X if possible. What is happenning is:

  1. Construct: the temporary allocates some space for the elements
  2. Move: the pointer is moved to X; the space used by X before is freed

Some objects overload the assignment operator to take an initializer list, and it is the case for std::vector and std::list. Such an operator may use the storage already allocated internally, which is the most effective solution here.

// Please insert the usual rambling about premature optimization here

1
Nicol Bolas On

Is it planned in future versions of C++?

No. And thank goodness for that.

Assignment is not the same as destroy-then-create. X is not destroyed in your assignment example. X is a live object; the contents of X may be destroyed, but X itself never is. And nor should it be.

If you want to destroy X, then you have that ability, using the explicit-destructor-and-placement-new. Though thanks to the possibility of const members, you'll also need to launder the pointer to the object if you want to be safe. But assignment should never be considered equivalent to that.

If efficiency is your concern, it's much better to use the assign member function. By using assign, the X has the opportunity to reuse the existing allocations. And this would almost certainly make it faster than your "destroy-plus-construct" version. The cost of moving a linked list into another object is trivial; the cost of having to destroy all of those allocations only to allocate them again is not.

This is especially important for std::list, as it has a lot of allocations.

Worst-case scenario, assign will be no less efficient than whatever else you could come up with from outside the class. And best-case, it will be far better.

4
G. Sliepen On

When you have a statement involving a move assignment:

x = std::move(y);

The destructor is not called for x before doing the move. However, after the move, at some point the destructor will be called for y. The idea behind a move assignment operator is that it might be able to move the contents of y to x in a simple way (for example, copying a pointer to y's storage into x). It also has to ensure its previous contents are destroyed properly (it may opt to swap this with y, because you know y may not be used anymore, and that y's destructor will be called).

If the move assignment is inlined, the compiler might be able to deduce that all the operations necessary for moving storage from y to x are just equivalent to in-place construction.

0
Cheers and hth. - Alf On

Re your final question

Am I right?

No.

Your ideas about what's permitted or not, are wrong. The compiler is permitted to substitute any optimization as long as it preserves the observable effects. This is called the "as if" rule. Possible optimizations include removing all of that code, if it does not affect anything observable. In particular, your "isn't allowed" for the second example is completely false, and the reasoning "it eliminates assignment" applies also to your first example, where you draw the opposite conclusion, i.e. there's a self-contradiction right there.