41

In The C++ programming language Edition 4 there is an example of a vector implementation, see relevant code at the end of the message.

uninitialized_move() initializes new T objects into the new memory area by moving them from the old memory area. Then it calls the destructor on the original T object, the moved-from object. Why is the destructor call necessary in this case?

Here is my incomplete understanding: moving an object means that the ownership of the resources owned by the moved-from object are transferred to the moved-to object. The remainings in the moved-from object are some possible members of built-in types that do not need to be destroyed, they will be deallocated when the the vector_base b goes out of scope (inside reserve(), after the swap() call). All pointers in the moved-from object are to be put to nullptr or some mechanism is employed to drop ownership of moved-from object on those resources so that we are safe, then why call the destructor on a depleted object when the "vector_base b" destructor will anyway deallocate the memory after the swap is done?

I understand the need to explicitly call the destructor in the cases when it must be called because we have something to destruct (e.g. drop elements) but I fail to see its meaning after the std::move + deallocation of vector_base. I read some texts on the net and I'm seeing the destructor call of the moved-from object as an signal (to whom or what?) that the lifetime of the object is over.

Please clarify to me what meaningful work remains to be done by the destructor? Thank you!

The code snippet below is from here http://www.stroustrup.com/4th_printing3.html

template<typename T, typename A>
void vector<T,A>::reserve(size_type newalloc)
{
    if (newalloc<=capacity()) return;                   // never decrease allocation
    vector_base<T,A> b {vb.alloc,size(),newalloc-size()};   // get new space
    uninitialized_move(vb.elem,vb.elem+size(),b.elem);  // move elements
    swap(vb,b);                                 // install new base 
} // implicitly release old space

template<typename In, typename Out>
Out uninitialized_move(In b, In e, Out oo)
{
    using T = Value_type<Out>;      // assume suitably defined type function (_tour4.iteratortraits_, _meta.type.traits_)
    for (; b!=e; ++b,++oo) {
        new(static_cast<void*>(&*oo)) T{move(*b)};  // move construct
        b->~T();                                // destroy
    }
    return oo;       
}
Community
  • 1
  • 1
cvomake
  • 537
  • 1
  • 4
  • 7
  • This is a generic implementation, you don't even now if `T{move(*b)}` calls a move-constructor. – dyp Dec 14 '13 at 22:38
  • Don't think about moving *too* much. To first approximation, moving is just like copying, and the world still plays by the same rules. – Kerrek SB Dec 14 '13 at 22:45
  • :-) but moving is very important exactly because it is not copying, which is too expensive to be done in some cases, so I have to think about it as different from copy. With copy I can decide to drop the original or not, with move I kind of say "I am now the new owner of your belongings and you are depleted" – cvomake Dec 14 '13 at 22:53

2 Answers2

49

Moving from an object just means that the moved-from object might donate its guts to live on in another live object shortly before it is [probably] going to die. Note, however, that just because an object donated its guts that the object isn't dead! In fact, it may be revived by another donating object and live on that object's guts.

Also, it is important to understand that move construction or move assignment can actually be copies! In fact, they will be copies if the type being moved happens to be a pre-C++11 type with a copy constructor or a copy assignment. Even if a class has a move constructor or a move assignment it may choose that it can't move its guts to the new object, e.g., because the allocators mismatch.

In any case, a moved from object may still have resources or need to record statistics or whatever. To get rid of the object it needs to be destroyed. Depending on the class's contracts it may even have a defined state after being moved from and could be put to new use without any further ado.

metal
  • 6,202
  • 1
  • 34
  • 49
Dietmar Kühl
  • 150,225
  • 13
  • 225
  • 380
  • Now that makes more sense ;) Maybe you could think of it as a living donation of a kidney. Not all guts are transplanted, and the donator might still live after the operation. – dyp Dec 14 '13 at 22:46
  • I have to admit that this brings new perspectives to my question. Still, the fact that the moved-from object might still have resources is a bit odd as this would mean the the move constructor does not keep the invariants of the class, or what kind of resources are you referring to? – cvomake Dec 14 '13 at 23:08
  • 1
    @cvomake: A copy is a valid move construction as long as the move doesn't guarantee that pointers or references into the old object stay valid as long as the new object stays valid. A move does _not_ guarantee that any resources are transferred. It merely indicates that it is save to transfer resources and, thus, many classes will aim at rather transferring than copying resources upon moves. – Dietmar Kühl Dec 14 '13 at 23:32
  • 13
    moved object is like zombie, you still need kill it :) – Yankes Dec 15 '13 at 01:28
  • 2
    @DyP: Your statement is one I have been trying to discredit for years now. A moved-from object must still have all of its invariants intact. The only thing a client of a moved-from object can count on is that the moved-from object is in a valid (invariants intact) but unspecified state. Now if you are the author of this class, you can define its invariants any way you want. Maybe your class has an invariant that amounts to a moved-from value can only be destructed or assigned to. – Howard Hinnant Dec 15 '13 at 01:44
  • 1
    A more general way to think about this is that a moved-from value can have any member called on it that has no preconditions. If all of your members have a precondition that the value is not in a moved-from state, so be it, that is your prerogative. But that is not a requirement of move semantics. std-defined types like `vector` can have any member called on them which has no precondition. This includes `empty()`, `clear()` and `size()`. – Howard Hinnant Dec 15 '13 at 01:46
  • @DyP: when the moved-to object gets constructed from the guts of the moved-from objects all class invariants must hold on the moved-to object. This is my understanding, the new moved-to object must be a "copy" of that old object, so it makes no sense that something will miss(no resource, no counters, no etc) otherwise we will have a violation of the class invariants. I cannot imagine a scenario(this does not prove anything, merely that I cannot find one) when a selective move is necessary and perhaps that is a strong assumption I make. Moving is copying with efficiency, or? – cvomake Dec 15 '13 at 09:50
  • @DietmarKühl I think I got my answer from your comment. You are basically saying that calling move() might result in a copy (it is T decision what to do, move or not to move) and we need to destroy the initial object (to achieve a complete move), right? I guess I was too focused on the move-only perspective and did not see the point of calling a meaningful destructor operations after a clean move. Thank you for the fresh perspective. – cvomake Dec 15 '13 at 10:44
  • @HowardHinnant Hmm; I see this holds for StdLib objects. I was thinking of, for example, a resource handle w/o an empty state (unlike `std::ifstream`, for example). When RAII (for `std::ifstream`, there's a way to initialize it without acquiring a resource), there's no possible valid state when the resource has been moved. By having an empty state, the moved-from object can still be in a valid state; maybe my "typically" therefore was too exaggerated and should have been "the absolute minimum you can rely on". Do you have a link where this is discussed in more detail? – dyp Dec 15 '13 at 13:52
  • @HowardHinnant Thank you very much, although I've already read it. I think I'll need to think more about class invariants vs. member function preconditions (what can/should a class guarantee), especially in the context of Resource Acquisition *Is* Initialization vs. Resource Acquisition *Implies* Initialization. Removing the provoking comment for now. – dyp Dec 15 '13 at 19:52
  • I recently considered the same problem as the OP and concluded that if I ensure (using `std::enable_if` or `static_assert` or similar) that the value_type is `std::is_nothrow_move_constructible`, then `std::move` cannot really leave anything worth destructing behind, and it should be safe to omit the placement destructors. Is this reasoning correct? – FaulerHase Sep 07 '15 at 10:31
  • @HowardHinnant Doesn't this whole approach just pessimize move operations? When I move an object out - I really do mean that I'm done with it completely. The "valid but unspecified" / "will still be destroyed" concept means that you still need to (a) do stuff to the old object and (b) call its destructor. Would be more efficient if the contract was "moved-from objects are already dead, no destructor will be called, any further access is ill-formed." – Barry Mar 04 '16 at 18:59
  • 2
    @Barry: Study the implementation of the general `std::swap` algorithm with your question in mind. That's not to say that "destructive move semantics" would not be useful. I'm sure it would. But if I had to choose between one of "constructive move semantics" and "destructive move semantics", I would choose the former, and that was exactly the choice we had a decade ago. See http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2002/n1377.htm#Alternative%20move%20designs for a few more details about this choice. – Howard Hinnant Mar 05 '16 at 15:21
  • It wasn't mentioned before, but the "guts" of the original object can be (at least patially) both not movable and not copyable. The insides would be created anew at the new location, but the old object should be destroyed. For example, you may create a movable wrapper to place mutexes in collections. – Sergey.quixoticaxis.Ivanov Jun 05 '18 at 00:18
0

In his proposal to implement move semantics through rvalue references, Howard Hinant considered the push for Destructive Move too. But as he pointed in the same article, a move operation basically deals with to objects (source an destination) simultaneously in transition state - even in single threaded applications. The problem with destructive move is that either source, or destination will experience a state in where derived subpart of an object is constructed while base subpart is destroyed or uninitialized. This was not possible prior to move proposal, and is not acceptable yet. So the other choice left was to leave the moved from object in a valid empty state and let the destructor remove the valid empty state. For many practical purposes, destroying valid empty state is a noop; But it's not possible to be generalized for all cases. So, the moved from object is supposed to be either destructed, or assigned(move or copy) to.

Red.Wave
  • 2,790
  • 11
  • 17