1

This simple code:

#include <iostream>
#include <vector>

struct my_struct
{
    int m_a;

    my_struct(int a) : m_a(a) { std::cout << "normal const " << m_a << std::endl; }

    my_struct(const my_struct&& other) : m_a(other.m_a) { std::cout << "copy move " << other.m_a << std::endl; }

    my_struct(const my_struct &other) : m_a(other.m_a) { std::cout << "copy const " << other.m_a << std::endl; }
};

class my_class
{
public:
    my_class() {}

    void append(my_struct &&m) { m_vec.push_back(m); }

private:
    std::vector<my_struct> m_vec;
};

int main()
{
    my_class m;

    m.append(my_struct(5));
    m.append(std::move(my_struct(6)));
}

produces this output:

normal const 5
copy const 5
normal const 6
copy const 6
copy const 5

The first call to append creates the object, and push_back creates a copy. Likewise, the second call to append creates the object, and push_back creates a copy. Now, a copy constructor of the first object is mysteriously called. Could someone explain me what happens? It looks like a strange side effect...

Mark Morrisson
  • 2,543
  • 4
  • 19
  • 25

2 Answers2

0

Now, a copy constructor of the first object is mysteriously called. Could someone explain me what happens? It looks like a strange side effect...

When you call push_back on std::vector, vector may need to grow it's size as stated in the cppreference:

If the new size() is greater than capacity() then all iterators and references (including the past-the-end iterator) are invalidated. Otherwise only the past-the-end iterator is invalidated.

You can use reserve before pushing anything to your vector. Try this:

class my_class
{
public:
    my_class()
    {
        m_vec.reserve(10); // Use any number that you want.
    }

    void append(my_struct &&m) { m_vec.push_back(m); }

private:
    std::vector<my_struct> m_vec;
};

Few other issues with your program:

  1. You need to fix signature of your move constructor as move constructor requires rvalue reference (more specifically, xvalue or prvalue). It should like this:

    my_struct(my_struct&& other) noexcept : m_a(other.m_a)
    {
        std::cout << "copy move " << other.m_a << std::endl;
    }
    

    noexcept is required as we need to inform C++ (specifically std::vector) that move constructor and destructor does not throw, using noexcept. Then the move constructor will be called when the vector grows. See this.

  2. The method append should be:

    void append(my_struct &&m)
    {
        m_vec.push_back(std::move(m));
    }
    

    To know why we need to use std::move on rvalue reference, see this Is an Rvalue Reference an Rvalue?. It says:

    Things that are declared as rvalue reference can be lvalues or rvalues. The distinguishing criterion is: if it has a name, then it is an lvalue. Otherwise, it is an rvalue.

    If you don't use std::move, then copy constructor would be called.

abhiarora
  • 9,743
  • 5
  • 32
  • 57
  • 1
    Also needs `void append(my_struct &&m) { m_vec.push_back(std::move(m)); }` or it loses the whole point of being a move-able. – Eljay May 31 '20 at 13:53
  • 1
    @Eljay and `noexcept` too – asmmo May 31 '20 at 13:54
  • should be `my_struct(my_struct&& other)noexcept : m_a(std::move(other.m_a)) { std::cout << "copy move " << other.m_a << std::endl; }` – asmmo May 31 '20 at 13:57
  • Added more details. Thanks. – abhiarora May 31 '20 at 14:04
  • Thank you very much for these explanations! I didn't think a second it could come from a reallocation of the vector. I'm very surprised that its initial capacity was… 0! I was convinced that it was something like 10, as in Java. In other words, using a vector without setting an initial capacity could be very costly. – Mark Morrisson May 31 '20 at 14:31
0

That's just how std::vector works! When you call push_back(), the underlying array needs to grow to make room for the new element. So internally, a new larger array is allocated and all the elements of the previous smaller array are copied into the freshly created array. This also comes with some overhead. Now, you can use some techniques to optimize away the copies.

If you have an idea of how large the array could grow, you can use the reserve() method to ensure that no resizing will occur upto that many locations.

vct.reserve(5)

This is will ensure that no resizing will occur until 5 elements. Also, you can use the emplace_back() function to avoid an additional copy. It constructs the object in place. Simply pass the constructor parameters of the object to emplace_back()

Amal K
  • 4,359
  • 2
  • 22
  • 44