Iman A. Fazel
Iman A. Fazel

Reputation: 58

Why move semantics have the same behavior as shallow copy in dynamic mem allocation?

In the classes dealing with dynamic mem allocation, shallow copy basically causes the program to delete a resource twice. In move operations, the original pointer no longer points to the resource, So But why the same behavior occurs in move semantics? e.g:

#include <utility>
#include <cstring>
using namespace std;
class MyString
{
    char* cstr;
 public:
    MyString(const char* arg)
    : cstr(new char[strlen(arg)+1])
    {
        strcpy(cstr, arg);
    }
    MyString(MyString&&) = default;
    MyString& operator=(MyString&&) = default;
    MyString(const MyString&) = delete;
    MyString& operator=(const MyString&) = delete;
    ~MyString()
    {
        delete[] cstr;
    }
};

int main()
{
    MyString S1{"aaa"};
    MyString S2 = move(S1); // error

}

I've tried with 3 different compilers and i got the same results.

Upvotes: 1

Views: 965

Answers (2)

Nicol Bolas
Nicol Bolas

Reputation: 473627

[why is] Moving a pointer (or any other primitive object) is same as copying it.

Because in C++, you do not pay for what you do not use (in general).

A naked pointer (aka: not a smart pointer) in C++ is assumed to be non-owning with regard to the object it points to. This is why you must delete it manually; the compiler doesn't generate an implicit delete statement for any pointers that fall out of scope.

Since a pointer does not own memory, why is it correct to null out the source of a pointer when you move one? It isn't. It's perfectly valid for a non-owning pointer's move to just copy the pointer value and leave the old one. Just like it's perfectly valid for a non-owning pointer to be destroyed without destroying what it points to.

C++ doesn't know that you need to delete that pointer, just as it doesn't know you need to null out the old one. If you need that, then you have to do it.

If you want to create an owning pointer, it is you who must create those semantics. These semantics are more expensive than non-owning semantics. A pointer move is just a bitwise-copy; a smart-pointer move has to run actual code. It has to do two memory store operations: storing the old value in the new pointer, and nulling-out the old pointer. That's more expensive than one.

And therefore, you have to explicitly ask for it or do it yourself.


To understand the potential effect on performance, consider a vector<T*>. Now, let's consider that this type is a private member of a class. This class dynamically allocates some Ts and puts them in the vector. It also ensures that any Ts added are deleted when the class is deleted.

So, let's consider what vector<T*> would have to look like with your proposed idea of moving a pointer causing the old value to become nullptr. One important element of vector is reallocation; when you insert more elements than the vector can hold, it has to allocate larger storage and move all of the elements to the new storage, then delete the older storage.

With your proposed idea, reallocating a vector<T*> would mean moving all of the pointer elements. But each move is two memory operations: copying the value and nulling out the old one. But the thing is, the old value? That value is about to be destroyed. And the class that owns the vector<T*> doesn't need to delete it or anything; it will be in the new storage. So there is no reason why vector<T*> needed to null out a value that's going to be deleted.

By contrast, a modern vector<T*> implementation will "move" the pointers by doing a single memcpy of the entire buffer to the new location. It can do this because pointers are trivially-copyable, which allows them to be both copied and moved by doing byte-wise copies.

So your way is much slower. And your way gains nothing, because the old value will be dropped when the memory is deallocated. So there's no need to null it out in that case.

C++ doesn't know when you're implementing vector or implementing a smart pointer. Therefore, it goes for the lowest-common-denominator: pointers are trivially copyable. If you want specialized move behavior, you must implement it.

Upvotes: 3

eerorika
eerorika

Reputation: 238381

The implicitly generated move constructor move-constructs each member. Your member is a pointer. Moving a pointer (or any other primitive object) is same as copying it.

So, since your move constructor does nothing else besides moves the pointer, the moved from MyString object will still be pointing to the same pointee object as the moved to MyString object. And when both are destroyed, the destructor will try to delete the pointee twice.

You need to follow the rule of 5: If any of destructor, move/copy constructor, move/copy assignment needs to be implemented, then all of them probably need to be implemented (or deleted). You've implemented the destructor to delete an owned pointer, so you must implement the move constructor (and the others) such that the moved from (or copied from) object no longer points to the object that it no longer owns.

Why move semantics have the same behavior as shallow copy in dynamic mem allocation?

Because moving is just another word for shallow copying. The move constructor and assignment operator are there to be given a custom implementation in case the moved from object needs to be cleaned up to maintain the class invariant. Just like the copy constructor and copy assignment operator are there to make a deep copy that does not break the class invariant.

Upvotes: 5

Related Questions