Why the copy constructor is called 25 times, while the insertion loop iterates only 10 times? [duplicate]
Asked Answered
S

3

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
Squadron answered 21/9, 2017 at 9:59 Comment(3)
That's reallocation. Probably from an implementation that doubles the capacity each time it reallocates. 1 + 2 + 4 + 8 = 15.Spue
The odd number comes from the internal memory re-allocation of the vector.Leathers
@Spue Yes, you are right. Thank you!Squadron
D
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

Dworman answered 21/9, 2017 at 10:2 Comment(7)
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?Sterigma
@tobi303: if that were the case then you wouldn't really have to reallocate...Dworman
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 copiesSterigma
@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!!Squadron
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.Maxentia
@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.Dworman
@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.Poacher
P
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.

Pesek answered 21/9, 2017 at 10:14 Comment(0)
S
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.

Stanfordstang answered 21/9, 2017 at 10:16 Comment(2)
Yes, it was a good point that you have mentioned. Thanks!Squadron
@BahramdunAdil: Thank you for your interesting question!Stanfordstang

© 2022 - 2024 — McMap. All rights reserved.