c++resizestdvectordestruction

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?


Solution

  • 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.