3

I have the following code:

#include <iostream>
#include <vector>
using namespace std;


int counter = 0;
struct vecobj
{
   int elem;
   vecobj(){ }
   vecobj(const vecobj& rhs)
   {
    counter++;
    elem = rhs.elem;
    
    
   }
};

vector<vecobj> objvec;

int main()
{
    vecobj obj1;
    obj1.elem = 1;
    objvec.push_back(obj1);
    objvec.push_back(obj1);
    objvec.push_back(obj1);
    std::cout <<"copy constructor called "<< counter <<" times\n";
}

My question is that when I push an object into the vector objvec, I get the copy constructor called 6 times. While I have pushed only three times. I have verified this with the debugger. I am using visual studio 2017 Community Edition. Is it a bug with STL? I think a related question does not address the problem fully. No where is it documented that the vector memory has to be reserved before pushing onto it.The pushing of object calling the copy constructor too many (fibonacci(n)) times can have unintended side effects. I think STL code needs to be changed for the same.

3 Answers3

4

Normally std::vector implementation have a capacity and a size variables. To track the current capacity means maximum number of elements that can be created without requesting a bigger heap block. 1size1 tracks the current size of std::vector.

When we push_back an element and size becomes greater than capacity then program will ask for new heap and copy all the elements to new location.

Capacity increases in power of 2. like initially space for 1 object/variable will be allocated, if application will need more then space for 2 objects/variables will be allocated, then for 4 and so on.

  1. Initially first copy happens when you push_back first element. (1 Copy)
  2. When you push_back second element then a new memory is allocated and the new element as well as the already push_backed element is copied to new location. (2 Copy)
  3. Similarly third time three copies will happen.

All of this is implementation dependent. Standard don't impose that std::vector should behave like this only.

To save the copies you can reserve the enough memory before you make any push_back call.

The Philomath
  • 954
  • 9
  • 15
  • small nitpick: It is not necessarily a factor of 2. A factor of 3 would be conforming as well. Actually any factor is fine to guarantee armortized constant complexity for `push_back` (which is the reason for needing to have a reallocation strategy that not just allocates enough for the current size) – 463035818_is_not_an_ai Jan 20 '21 at 11:07
  • @largest_prime_is_463035818, You are right its implementation dependent . I have added that line in answer. – The Philomath Jan 20 '21 at 11:11
  • 1
    …and a good example is MSVC, which does not use factors of 2. – spectras Jan 20 '21 at 11:30
  • 1
    @largest_prime_is_463035818 `Actually any factor is fine` Are you sure? Surely any factor less than or equal to 1 won't be fine :) – eerorika Jan 20 '21 at 12:49
  • @eerorika always glad to see that I am not the only nitpicker :P. "Any constant integer greater or equal than 2" is less wrong I believe ;) – 463035818_is_not_an_ai Jan 20 '21 at 13:00
  • @largest_prime_is_463035818 I've read MSVC uses 1.5. I've pondered occasionally, but never bothered to do the math to prove or disprove whether any or all factors in the interval ]1, 2[ achieve amortised constant complexity. – eerorika Jan 20 '21 at 13:03
  • @largest_prime_is_463035818 Feel Free to update the answer if you think any additional info can help the seekers. – The Philomath Jan 20 '21 at 13:05
  • @eerorika oh well, msvc not using 2 was new to me. 1.5 is another surprise I have to admit. I remeber an article that discussed the requirement of amortized constant push_back and the resulting requirement on the factor in all glory details. What I kept in memory was a simple "anything >= 2 is fine", I suppose that was wrong. Thanks for the clarification – 463035818_is_not_an_ai Jan 20 '21 at 13:06
4

You already have the trivial 3 copies here:

objvec.push_back(obj1);
objvec.push_back(obj1);
objvec.push_back(obj1);

Then you might check capacity (depend of implementation), here I have:

std::cout << "capacity: " << objvec.capacity() << std::endl; // 0
objvec.push_back(obj1);
std::cout << "capacity: " << objvec.capacity() << std::endl; // 1
objvec.push_back(obj1);
std::cout << "capacity: " << objvec.capacity() << std::endl; // 2
objvec.push_back(obj1);
std::cout << "capacity: " << objvec.capacity() << std::endl; // 4

Each time capacity changes, reallocation happens, and we have to transfer data from old buffer to the new one. so copy (or move).

You might reduce copy by using correct capacity from the beginning:

vecobj obj1;
obj1.elem = 1;

objvec.reserve(3);
objvec.push_back(obj1);
objvec.push_back(obj1);
objvec.push_back(obj1);

Demo

Jarod42
  • 203,559
  • 14
  • 181
  • 302
0

Lets put moves aside (your elements cannot be moved), then push_back copies at least one element to insert it into the vector. If the vectors capacity is not enough to hold the additional element, then the vector has to resize. While doing that it copies all elements. Typical resize strategies allocate space for elements by factors of two. So this is possibly what happens:

 vecobj obj1;               // size 0 capacity 0
obj1.elem = 1;
objvec.push_back(obj1);     // size 1 capacity 1   -> 1 copy to insert
objvec.push_back(obj1);     // size 2 capacity 2   -> 1 copy to insert 1 to reallocate
objvec.push_back(obj1);     // size 3 capacity 4   -> 1 copy to insert 2 to reallocate

Which makes a total of 6 copies.

463035818_is_not_an_ai
  • 109,796
  • 11
  • 89
  • 185