Search code examples
c++c++11vectormove-semantics

does std::vector copy/move elements when re-sizing?


I was messing around with move c'tors for a learning / refresh exercise and I came across something unexpected to me. Below I have a class person that contains a std::string m_name;. I am using this as a test class for copy/move c'tors.

Here is the code for quick reference:

#include <iostream>
#include <vector>

class person
{
public:
    std::string m_name;
    explicit person(const std::string &name) : m_name(name)
    {
        std::cout << "created " << m_name << std::endl;     
    }
    
    ~person()
    {
        std::cout << "destroyed " << m_name << std::endl;       
    }   
    
    person(const person &other) : m_name(other.m_name)
    {
        m_name += ".copied";
        std::cout << "copied " << other.m_name << " -> " << m_name << std::endl;
    }
    
    person(const person &&other) noexcept : m_name(std::move(other.m_name))
    {
        m_name += ".moved";
        std::cout << "moved " << other.m_name << " -> " << m_name << std::endl;
    }   
};

int main()
{
    std::vector<person> people;
    people.reserve(10);
    
    std::cout << "\ncopy bob (lvalue):" << std::endl;
    person bob{"bob"};
    people.push_back(bob);

    std::cout << "\nmove fred (lvalue):" << std::endl;
    person fred{"fred"};
    people.push_back(std::move(fred));

    std::cout << "\ntemp joe (rvalue):" << std::endl;
    people.push_back(person{"joe"});
    
    std::cout << "\nterminating:" << std::endl;
}

This gives me the output that I would expect (mostly, except why std::string contents is not "moved"?): https://godbolt.org/z/-J_56i

Then I remove the std::vector reserve so that std::vector has to "grow" as I am adding elements. Now I get something that I really don't expect: https://godbolt.org/z/rS6-mj

Now I can see that bob is copied and then moved when fred is added and then moved again when joe is added. I was under the impression that std::vector "moved" when it has to reallocate space. But I thought that it did a memory copy/move, not an object-by-object copy/move. I really did not expect it to call the move constructor.

Now if I remove the move c'tor, I find that bob is copied three times!: https://godbolt.org/z/_BxnvU This seems really inefficient.

From cplusplus.com:

push_back()

Add element at the end Adds a new element at the end of the vector, after its current last element. The content of val is copied (or moved) to the new element.

This effectively increases the container size by one, which causes an automatic reallocation of the allocated storage space if -and only if- the new vector size surpasses the current vector capacity.

resize()

Resizes the container so that it contains n elements.

If n is smaller than the current container size, the content is reduced to its first n elements, removing those beyond (and destroying them).

If n is greater than the current container size, the content is expanded by inserting at the end as many elements as needed to reach a size of n. If val is specified, the new elements are initialized as copies of val, otherwise, they are value-initialized.

If n is also greater than the current container capacity, an automatic reallocation of the allocated storage space takes place.

Notice that this function changes the actual content of the container by inserting or erasing elements from it.

I guess it does not really describe "how" it does the reallocation, but surely a memory copy is the fastest way to move the vector to its newly allocated memory space?

So why are the copy/move c'tors called when std::vector is added to instead of a memory copy?

A side note/question: (any maybe this should be a separate question): In person move c'tor why is moved fred -> fred.moved printed and not moved -> fred.moved. It appears that the std::string move assignment does not really "move" the data...


Solution

  • If it needs to relocate, something similar to std::move(xold.begin(), xold.end(), xnew.begin()); will be used. It depends on the value type and the vector usually does its own internal placement new . but it'll move if it can move.

    Your move constructor

    person(const person &&other) noexcept;
    

    has a flaw though: other should not be const since it must be allowed to change other to steal its resources. In this move constructor

    person(person&& other) noexcept : m_name(std::move(other.m_name)) {}
    

    the std::strings own move constructor will do something similar to this:

    string(string&& other) noexcept : 
        the_size(other.the_size),
        data_ptr(std::exchange(other.data_ptr, nullptr))
    {}
    

    You also need to add a move assignment operator:

    person& operator=(person &&other) noexcept;