6

I am wondering why in the following C++ code the copy constructor is called 25 times for 10 iterations?

If it was 10 then OK 10/10 = 1, or 20/10 = 2, or 30/10 = 3, but 25/10 = 2.5? What does the .5 here mean?

Header:

class Person
{
public:
    Person(std::string name, int age);
    Person(const Person &person);

    const std::string &getName() const;
    int getAge() const;

private:
    std::string name;
    int age;
};

Source:

Person::Person(string name, int age) : name(std::move(name)), age(age)
{}

Person::Person(const Person &person)
{
    this->name = person.name;
    this->age = person.age;
    static int count = 0;
    count++;
    cout << ">>Copy-Person::Person(Person &person) " << count << endl;
}

const string &Person::getName() const
{
    return name;
}

int Person::getAge() const
{
    return age;
}

Usage:

int main()
{
    vector<Person> persons;

    for (int i = 0; i < 10; ++i)
    {
        Person person(to_string(i + 1), i);
        persons.push_back(person);
    }
    cout << "-----------------------------------------------" << endl;
    for (Person &person : persons)
    {
        cout << "name = " << person.getName() << " age = " << person.getAge() << endl;
    }
    return 0;
}

Output:

>>Copy-Person::Person(Person &person) 1
>>Copy-Person::Person(Person &person) 2
>>Copy-Person::Person(Person &person) 3
>>Copy-Person::Person(Person &person) 4
>>Copy-Person::Person(Person &person) 5
>>Copy-Person::Person(Person &person) 6
>>Copy-Person::Person(Person &person) 7
>>Copy-Person::Person(Person &person) 8
>>Copy-Person::Person(Person &person) 9
>>Copy-Person::Person(Person &person) 10
>>Copy-Person::Person(Person &person) 11
>>Copy-Person::Person(Person &person) 12
>>Copy-Person::Person(Person &person) 13
>>Copy-Person::Person(Person &person) 14
>>Copy-Person::Person(Person &person) 15
>>Copy-Person::Person(Person &person) 16
>>Copy-Person::Person(Person &person) 17
>>Copy-Person::Person(Person &person) 18
>>Copy-Person::Person(Person &person) 19
>>Copy-Person::Person(Person &person) 20
>>Copy-Person::Person(Person &person) 21
>>Copy-Person::Person(Person &person) 22
>>Copy-Person::Person(Person &person) 23
>>Copy-Person::Person(Person &person) 24
>>Copy-Person::Person(Person &person) 25
-----------------------------------------------
name = 1 age = 0
name = 2 age = 1
name = 3 age = 2
name = 4 age = 3
name = 5 age = 4
name = 6 age = 5
name = 7 age = 6
name = 8 age = 7
name = 9 age = 8
name = 10 age = 9
Mr.C64
  • 41,637
  • 14
  • 86
  • 162
Bahramdun Adil
  • 5,907
  • 7
  • 35
  • 68

3 Answers3

4

You are not reserving any memory for your persons vector. This means that when persons.size() == persons.capacity() during a push_back, the vector will allocate a new bigger buffer on the heap and copy every element to it. This is why you see more copies than expected.

If you write...

persons.reserve(10); 

...before the loop, you will not see any "extra" copy.

live example on wandbox


Note that you can avoid the copies altogheter by using both std::vector::emplace_back and std::vector::reserve:

for (int i = 0; i < 10; ++i)
{
    persons.emplace_back(to_string(i + 1), i);
}

This will only print:

name = 1 age = 0

name = 2 age = 1

name = 3 age = 2

name = 4 age = 3

name = 5 age = 4

name = 6 age = 5

name = 7 age = 6

name = 8 age = 7

name = 9 age = 8

name = 10 age = 9

live example on wandbox

Vittorio Romeo
  • 90,666
  • 33
  • 258
  • 416
  • is reallocation always a copy to a different memory location? or can it happen that there is some free memory just after that occupied by the elements, such that they dont need to be copied? – 463035818_is_not_an_ai Sep 21 '17 at 10:03
  • @tobi303: if that were the case then you wouldn't really have to reallocate... – Vittorio Romeo Sep 21 '17 at 10:04
  • errm right, my wording was off. Another try: Is it possible that the capacity increases without needing to reallocate? Btw for the `emplace_back` imho you should mention that it needs `reserve` plus `emplace_back`. Atm it could be misunderstood as `emplace_back` alone preventing all copies – 463035818_is_not_an_ai Sep 21 '17 at 10:06
  • @VittorioRomeo Hmmm.Thanks! It makes sense. I have `reserve` + `emplace_back` no copy now. But one more question is it good that store the object itself to the vector or reference or pointer? I am coming from Java, so in Java, you can directly store objects in the List, what about C++? Thanks Again!! – Bahramdun Adil Sep 21 '17 at 10:13
  • `vector` cannot do the equivalent of `realloc` and maybe keep the same starting address with greater extent: the allocator interface doesn't support it, and it wouldn't allow copy/move construction and destruction of the old instances to work correctly in the case where the start address _does_ move. – Useless Sep 21 '17 at 10:15
  • 1
    @BahramdunAdil: C++ is very different from Java. Do not use pointers and dynamic allocation unless you really have to. Prefer the stack and value semantics whenever possible. – Vittorio Romeo Sep 21 '17 at 10:15
  • @Useless: An implementation *could* provide a specialization for ``std::allocator`` and use some platform-specific extension to try growing the heap block. As you pointed out, ``realloc`` doesn't cut it for non-POD types. – Arne Vogel Sep 21 '17 at 14:32
2

When the new size() > capacity() of the vector, reallocation happens. All the elements will be copied to the new internal storage, then the copy constructor will be called at the current elements' number of times. The details about how the capacity is increased depends on the implementation, it seems the implementation you're using just double the capacity for every reallocation. so

#iterator current size  capacity  times of the copy (for reallocatioin + for push_back)
1         0             0         0 + 1             
2         1             1         1 + 1             
3         2             2         2 + 1             
4         3             4         0 + 1             
5         4             4         4 + 1             
6         5             8         0 + 1             
7         6             8         0 + 1             
8         7             8         0 + 1             
9         8             8         8 + 1             
10        9             16        0 + 1             

That's why you got the result of 25 times.

As @VittorioRomeo explained, you can use std::vector::reserve to avoid reallocation.

songyuanyao
  • 169,198
  • 16
  • 310
  • 405
2

When std::vector::size() reaches std::vector::capacity(), std::vector will make room for more objects, allocating a new larger buffer with bigger capacity, and copying the previously stored objects into the new buffer.
This triggers new copy constructor calls for your Person class (I tried your code with VS2015, and I got 35 copy constructor calls).

Note that, if you reserve enough room in the std::vector with the reserve() method, you get exactly 10 copy constructor calls:

vector<Person> persons;

// Reserve room in the vector to store 10 persons
persons.reserve(10);

for (int i = 0; i < 10; ++i)
{
    Person person(to_string(i + 1), i);
    persons.push_back(person);
}

That's because, in this case, you made enough room in the vector, so the vector's size doesn't exceed its capacity (so, there's no need to allocate a new larger buffer, and copying the old data to this new buffer).

All that being said, if your Person class is move-constructible, std::vector will move the previously created Person objects instead of copying them, which is faster.

If you add this line inside your Person class:

class Person 
{
  public:
   ...

   // Synthesize default move constructor
   Person(Person&&) = default;
   ...
};

you'll get exactly 10 copy constructor calls, even if you don't call the vector::reserve() method.

Mr.C64
  • 41,637
  • 14
  • 86
  • 162