Search code examples
c++c++11stdvectormove-semanticsmove-constructor

Why are my data elements being copied instead of moved?


I'm performing some tests about move semantics, and my class behavior seems weird for me.

Given the mock class VecOfInt:

class VecOfInt {
public:
    VecOfInt(size_t num) : m_size(num), m_data(new int[m_size]) {}
    ~VecOfInt() { delete[] m_data; }
    VecOfInt(VecOfInt const& other) : m_size(other.m_size),  m_data(new int[m_size]) {
        std::cout << "copy..." <<std::endl;
        std::copy(other.m_data, other.m_data + m_size, m_data);
    }
    VecOfInt(VecOfInt&& other) : m_size(other.m_size) {
        std::cout << "move..." << std::endl;
        m_data = other.m_data;
        other.m_data = nullptr;
    }
    VecOfInt& operator=(VecOfInt const& other) {
        std::cout << "copy assignment..." << std::endl;
        m_size = other.m_size;
        delete m_data;
        m_data = nullptr;
        m_data = new int[m_size];
        m_data = other.m_data;
        return *this;
    }
    VecOfInt& operator=(VecOfInt&& other) {
        std::cout << "move assignment..." << std::endl;
        m_size = other.m_size;
        m_data = other.m_data;
        other.m_data = nullptr;
        return *this;
    }
private:
    size_t m_size;
    int* m_data;
};
  1. OK CASE

    When I insert a single value in-place:

    int main() {
        std::vector<VecOfInt> v;
        v.push_back(10);
        return 0;
    }
    

    Then it gives me the following output (what I think is fine):

    move...

  2. WEIRD CASE

    When I insert three different values in-place:

    int main() {
        std::vector<VecOfInt> v;
        v.push_back(10);
        v.push_back(20);
        v.push_back(30);
        return 0;
    }
    

    Then the output calls the copy constructor 3 times:

    move... move... copy... move... copy... copy...

What I'm missing here?


Solution

  • Move construction and move assignment aren't used by std::vector when reallocating unless they are noexcept or if there is no copying alternatives. Here is your example with noexcept added :

    class VecOfInt {
    public:
        VecOfInt(size_t num) : m_size(num), m_data(new int[m_size]) {}
        ~VecOfInt() { delete[] m_data; }
        VecOfInt(VecOfInt const& other) : m_size(other.m_size),  m_data(new int[m_size]) {
            std::cout << "copy..." <<std::endl;
            std::copy(other.m_data, other.m_data + m_size, m_data);
        }
        VecOfInt(VecOfInt&& other) noexcept : m_size(other.m_size) {
            std::cout << "move..." << std::endl;
            m_data = other.m_data;
            other.m_data = nullptr;
        }
        VecOfInt& operator=(VecOfInt const& other) {
            std::cout << "copy assignment..." << std::endl;
            m_size = other.m_size;
            delete m_data;
            m_data = nullptr;
            m_data = new int[m_size];
            m_data = other.m_data;
            return *this;
        }
        VecOfInt& operator=(VecOfInt&& other) noexcept {
            std::cout << "move assignment..." << std::endl;
            m_size = other.m_size;
            m_data = other.m_data;
            other.m_data = nullptr;
            return *this;
        }
    private:
        size_t m_size;
        int* m_data;
    };
    

    A live live example outputs :

    move...
    move...
    move...
    move...
    move...
    move...
    

    This is done to keep exception safety. When resizing a std::vector fails, it will try to leave the vector as it was before the attempt. But if a move operation throws half way through reallocation, there is no safe way to undo the moves that have already been made successfully. They very well may also throw. The safest solution is to copy if moving might throw.