2

Why does the C++ standard require elements to be move assignable (MoveAssignable) when using for example std::vector::erase? Why does one not use in-place construction (a.k.a. placement new) using the move constructor (MoveInsertable) for the elements that need to shift positions as is the case when adding elements to the std::vector exceeds the capacity of the container and internally a new and larger block of memory is allocated? Conceptually (imho), adding elements to and removing elements of a std::vector seem to be dual, but very similar operations with regard to the container management. Could therefore, someone clarify and explain the motivations behind the not-so-very similar operations actually used (std::vector::erase's MoveAssignable requirement vs. std::vector::emplace_back's MoveInsertable requirement)?

Imho, the MoveAssignable requirement is quite severe, since for most objects I do not see the need for assignment operators (copy or move) (when not considering the possibility of adding these objects to a std::vector). Furthermore, this also seems to deprecate the usage of const member variables (which imposes both constness from the perspective of the class interface and the internal class implementation as opposed to just const accessor member methods which can only impose constness from the perspective of the class interface and not from the internal class implementation).

Matthias
  • 4,481
  • 12
  • 45
  • 84
  • It might be because when you move-assign you may need to call a destructor (the existing value to be overidden). This is not the case when you move-construct. If you relocate a vector, the new location is uninitialized memory, so nothing needs to be destructed. In the case of `const` elements, it just falls back to a copy-assignment or copy-construction. – Jorge Bellon Mar 08 '18 at 10:55
  • @JorgeBellón you are right that one or more destructors may need to be called in case of `std::vector::erase`, but once called, you still have the raw memory available for placement new. – Matthias Mar 08 '18 at 11:02

2 Answers2

4

Why does the C++ standard require elements to be move assignable (MoveAssignable) when using for example std::vector::erase?

For performance, and for exception-safety.

Why does one not use in-place construction (a.k.a. placement new) using the move constructor (MoveInsertable) for the elements that need to shift positions as is the case when adding elements to the std::vector exceeds the capacity of the container and internally a new and larger block of memory is allocated? Conceptually (imho), adding elements to and removing elements of a std::vector seem to be dual, but very similar operations with regard to the container management.

No, not really.

When you remove an element you don't need to create any new objects. You are strictly decreasing the number of elements, so calling a constructor is not necessary. When you add elements, obviously you need to construct objects (at least the new ones you insert, and if the vector reallocates then you need to construct every element in the new location).

Could therefore, someone clarify and explain the motivations behind the not-so-very similar operations actually used (std::vector::erase's MoveAssignable requirement vs. std::vector::emplace_back's MoveInsertable requirement)?

You could implement erasure by destroying all the elements that get affected, but doing so can be less efficient. Consider a vector<X> where X manages a large block of memory, and only supports copying, not efficient moving. If you destroy each one and recreate a new object at the same location with placement new then the memory already owned by each element gets deallocated, then new memory reallocated again. If the block of memory owned by each X is the same size this is extremely wasteful: for every element you deallocate memory, then allocate exactly the same amount again. If you implement it with assignment then there is no reallocation: the element already has the required amount of storage, so it can just copy the data from one element to the next, into the memory it already has.

You're also running a destructor and a constructor for each element, rather than just an assignment operator.

But more importantly, if constructing the new element fails by throwing an exception, then you are left with a "hole" in the middle of the vector. You've destroyed an element, but not constructed a new one in its place. This breaks the invariant of the container. If you use assignment instead there is never a hole in the vector containing a dead object. If the assignment throws then the source and the target are both still valid objects, because no destructor was run yet.

Imho, the MoveAssignable requirement is quite severe, since for most objects I do not see the need for assignment operators (copy or move) (when not considering the possibility of adding these objects to a std::vector).

I would say that in general, absent any other requirements, your types should be assignable (concrete types should be regular). That's certainly true for "value types" that can be stored in containers. So if you want to use your types in standard containers, they need to model the necessary operations.

Assignability is not some esoteric weird property, it should be a default behaviour that most objects provide. Types that are not "regular" should be the exception, not the norm.

Furthermore, this also seems to deprecate the usage of const member variables (which imposes both constness from the perspective of the class interface and the internal class implementation as opposed to just const accessor member methods which can only impose constness from the perspective of the class interface and not from the internal class implementation).

How do const member functions not impose constness on the data members?

Jonathan Wakely
  • 166,810
  • 27
  • 341
  • 521
  • 1
    "How do const member functions not impose constness on the data members?" Those functions will still impose constness, but in non-const functions you can do everything you want with non-const member variables. You can of course document what is allowed or not, or explicitly validate invariants, but my point is that making the member variables const themselves is far more secure and compulsory than only imposing constness via the interface. – Matthias Mar 08 '18 at 12:40
  • "Assignability is not some esoteric weird property, it should be a default behaviour that most objects provide. Types that are not "regular" should be the exception, not the norm." From a pure functional programming point of view, mutability is rather esoteric. Though, in that case you wouldn't erase elements from an existing container either :) . – Matthias Mar 08 '18 at 12:43
  • C++ is not a pure functional language, and as you say, complaining that mutating a container requires mutability is a little odd :-) – Jonathan Wakely Mar 08 '18 at 12:52
2

After the move operation, it's still required to call then destructor (see this) on the objects that had the content "moved" out from.

So, in order to call the the move constructor a destructor must be called first on that memory location. This makes the following operations almost equivalent:

version 1:

first->~Element();
new(first) Element(std::move(*second))

and version 2:

*first = std::move(second); 
//inside operator =(&&), `this` object must be destroyed before doing the actual move;

For version 2 you must destroy first your existing object otherwise you can end up leaking memory. This is basically calling the destructor on yourself.

Regarding the puch_back and other reallocation operations the procedure is slightly different because the move constructor is called first and then the destructor on the old vector. This is "optimal" way of doing it, otherwise using assignment would require an empty constructor, a move assignment and (with an internal "destructor") and another destructor which is far from "optimal".

Technically you can use version 1 for erase operation, but it might not be optimal because the user can whatever he wants inside that assignment and might end up performing fewer operations than a separate destructor and constructor.

It is common for people to implement operator = (T &&) using version 1 because it removes redundant code. Another alternative is the swap trick (see boost intrusive_ptr implementation).

{ Element(std::move(*second)).swap(*first); }
// let's break down what's happening here in order:
// move constructor, swap, destructor on local element(containing original content of `first`)
Raxvan
  • 6,257
  • 2
  • 25
  • 46