user7769147
user7769147

Reputation: 1659

How does vector not destroy element twice after reducing its size?

For testing purposes, I was trying to create my own vector class, and I couldn't figure out how std::vector size reduction works.

class A
{
    A()
    { std::cout << "A constructed") << std::endl; }
    ~A()
    { std::cout << "A destroyed") << std::endl; }
}

main()
{
    std::vector<A> vec(3, A());
    vec.resize(2);
    std::cout << "vector resized" << std::endl;
}

Output is

A constructed       (1)
A constructed       (2)
A constructed       (3)
A destroyed         (1)
Vector resized
A destroyed         (2)
A destroyed         (3)

When vec.resize(2) is called, the third element is destroyed, but the vector's capacity is still 3. Then when vec is destroyed, all of its elements including the one already destroyed should be destroyed. How does std::vector know that he has already destroyed that element? How can I implement that in my vector class?

Upvotes: 2

Views: 218

Answers (3)

Remy Lebeau
Remy Lebeau

Reputation: 598134

When vec.resize(2) is called, the third element is destroyed, but the vector's capacity is still 3.

Yes. The capacity is how many elements the vector's internal array can physically hold. The size is how many elements in that array are actually valid. Shrinking the size does not affect the capacity at all.

Then when vec is destroyed, all of its elements including the one already destroyed should be destroyed.

The 3rd element that was previously destroyed and removed from the array is not destroyed again. Only size number of elements are destroyed, not capacity number of elements, like you are thinking.

How does std::vector know that he has already destroyed that element?

It tracks the size and capacity separately. When an element is removed from the array, the elements that follow it are moved down the array by 1 slot each, and the size is decremented.

Upvotes: 1

Quimby
Quimby

Reputation: 19223

There's a difference between capacity and size. Given a std::vector<T> v; The vector has allocated memory for v.capacity() elements. But only in the first v.size() places contain constructed T objects.

So, v.reserve(1000) on an empty vector won't call any additional constructors. vec.resize(2) in your example destroys the last element and vec[2] is now an empty place in memory, but memory still owned by the vec.

I think your allocation looks like buffer = new T[newSize];. That is not how std::vector works which would not allow Ts that do not have default constructors. Maybe you did not realize this, but whenever you obtain a piece of memory, it already contains objects, let it be T x; or even new double[newSize]; which returns an array of doubles (although their constructors are empty).

There is only one way to get usable uninitialized memory in C++, which is to allocate chars. This is due to the strict aliasing rule. There also (is|must be) a way how to call a constructor explicitly on this memory i.e. how to create an object there. The vector uses something called placement new which does precisely that. The allocation is then simply buffer = new char[newSize*sizeof(T)]; which creates no objects whatsoever.

The lifetime of the objects is managed by this placement new operator and explicit calls to destructors. emplace_back(arg1,arg2) could be implemented as {new(buffer + size) T(arg1,arg2);++size;}. Note that simply doing buffer[size]=T(arg1,arg2); is incorrect and UB. operator= expects that the left size(*this) already exists.

If you want to destroy an object with e.g. pop_back, you must do buffer[size].~T();--size;. This is one of the very few places where you should call the destructor explicitly.

Upvotes: 2

Gem Taylor
Gem Taylor

Reputation: 5635

The simple answer is that vector internally manages constructor and destructor calls, often by using inplace operator new and operator delete methods.

Having popped an element, it is immediately descructed, and while the memory is still available, and may still contain some remnant values, std::vector itself knows which elements still need to be deleted, and won't call the destructor again.

Upvotes: 1

Related Questions