Why is there no movement of objects in the vector when additional memory is reserved?

Viewed 91

There is the following code:

#include <iostream>
#include <vector>

class Test {
   public:
    Test() {}
    Test(int x) : x(x) {}

    Test(const Test&) noexcept = delete;
    Test& operator=(const Test&) noexcept = delete;

    Test(Test&& r) noexcept { x = std::move(r.x); }
    Test& operator=(Test&& r) noexcept {
        std::cout << "move";
        x = std::move(r.x);

        return *this;
    }

    ~Test() noexcept { std::cout << x; }

   private:
    int x;
};

int main() {
    std::vector<Test> v;

    v.reserve(5);
    v.emplace_back(1);
    v.emplace_back(2);
    v.emplace_back(3);
    v.emplace_back(4);
    v.emplace_back(5);
    v.reserve(10);
}

output:

1234512345

We see the destructors, but do not see the move assignment, so we can conclude that the object is being copied instead of moved. This question has already been discussed in several topics, but nowhere is there a specific answer.

There is a workaround using unique_ptrs:

#include <iostream>
#include <vector>
#include <memory>

class Test {
   public:
    Test() {}
    Test(int x) : x(x) {}

    Test(const Test&) noexcept = delete;
    Test& operator=(const Test&) noexcept = delete;

    Test(Test&& r) noexcept { x = std::move(r.x); }
    Test& operator=(Test&& r) noexcept {
        std::cout << "move";
        x = std::move(r.x);

        return *this;
    }

    ~Test() noexcept { std::cout << x; }

   private:
    int x;
};

int main() {
    std::vector<std::unique_ptr<Test>> v;

    v.reserve(5);
    v.emplace_back(std::make_unique<Test>(1));
    v.emplace_back(std::make_unique<Test>(2));
    v.emplace_back(std::make_unique<Test>(3));
    v.emplace_back(std::make_unique<Test>(4));
    v.emplace_back(std::make_unique<Test>(5));
    v.reserve(10);
}

output:

12345

All code tested on compilers gcc 5.1 - 10.2

Why does copying of objects occur when allocating memory in a vector instead of moves? Why is the move semantics not used in the first block of code, what is the reason for this behavior?

2 Answers

The statement v.reserve(10); causes your vector to reallocate its storage so it can accommodate at least 10 elements. Presuming capacity is less than 10, this is a four step process :

  1. New storage is allocated, enough so capacity will be at least 10.

  2. The elements from the previous storage need to be moved to the new storage. For each element in the previous storage, move that element's value into a new instance in the new storage.

  3. Before the old storage can be freed, the moved-from instances still need to be destroyed. This causes each of their destructor to execute, which prints 12345 in your case. Notice that the moved-from instances each still have their old value of x. Moving an int is equivalent to copying it, the original value is unchanged.

  4. The previous storage is now freed. The vector now uses the new storage, which contains equivalent elements but is larger than the previous storage was.

Finally, at the end of main your vector v has to be destroyed. This causes each of its elements to be destroyed as well, which causes each of the destructors to execute. This is the second set of 12345 that is printed.

With std::unique_ptr you only see one set of numbers (12345) because unique_ptrs are moved between the two storage, not Tests. The new moved-constructed instances of std::unique_ptr<Test> take ownership of the pointer and the old moved-from instances no longer own those pointers. When the instances from the previous storage are destroyed, their destructors do nothing (because they no longer own a pointer). You only see the destruction of your Test instances when v is destroyed at the end of main. Then, the std::unique_ptr instances that are destroyed do own pointers to Test, so it results in Test instances being deleted.

Edit : Note that you only instrumented your move assignment operator. std::vector will use the move constructor in this case, so you will not see "move" even when instances of Test are moved by v.

Move constructor/operator should leave old object in an undefined but valid state. The compiler in this case chooses to leave it unchanged and simply copy the values. When each of these sets of objects get destructed (first after move and second with the std::vector destructor), they print the values you see.

Related