Home > other >  how does std::vector deal with the call to destructor?
how does std::vector deal with the call to destructor?

Time:11-30

When tring to implement std::vector, I get confused with the implicit call to destructor.

Then element in vector maybe:

  • T
    • a Class Object,
  • T*, shared_ptr<T>
    • a Smart/Simple Pointer to Class Object
  • int
    • built-in type
  • int *
    • pointer to built-in type

When calling resize(),reserve() ,erase()or pop_back(), the destructor might be called.

I wonder how does std::vector deal with the call to destructor.

I found, only when the type is a build-in pointer won't std::vector call the destructor(Of course if it have one).

Does std::vector implement it by distinguishing type and determine whether or not to call the destructor?

Below are some experiments about the question:

Example 1, the element is Object.

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

struct Tmp {
    ~Tmp() { cerr << "Destructor is called." << endl; }
};

int main (void)
{
    std::vector<Tmp>arr;
    Tmp tmp = Tmp();
    cerr << "Capacity:" << arr.capacity() << endl;//0
    arr.push_back (tmp);
    cerr << "Capacity:" << arr.capacity() << endl;//1
    arr.push_back (tmp);
    cerr << "Capacity:" << arr.capacity() << endl;//2
    cerr << "popback start------------" << std::endl;
    arr.pop_back();
    arr.pop_back();
    cerr << "popback end--------------" << endl;
}

the output is:

Capacity:0
Capacity:1
Destructor is called.
Capacity:2
popback start------------
Destructor is called.
Destructor is called.
popback end--------------
Destructor is called.

Example 2, the element is builtin-in pointer to obecjt:

...
std::vector<Tmp>arr;
Tmp * tmp = new Tmp;
...

The destructor won't be called automatically:

Capacity:0
Capacity:1
Capacity:2
popback start------------
popback end--------------

Example 3, shared_ptr

std::vector<shared_ptr<Tmp>>arr;
auto tmp = make_shared<Tmp>();

... //after being copied, the references count should be 3.
tmp = nullptr; //References count reduced by 1

cerr << "popback start------------" << std::endl;
arr.pop_back();//References count reduced by 1
arr.pop_back();//References count reduced by 1
cerr << "popback end--------------" << endl;

The destructor of shared_ptr will be called. When the references reduced to 0, the destructor of Tmp will be called:

Capacity:0
Capacity:1
Capacity:2
popback start------------
Destructor is called.
popback end--------------

CodePudding user response:

Lets say you have the pointer defined by:

Tmp * tmp = new Tmp;

This can be illustrated like this:

 --------------        ------------ 
| variable tmp | ---> | Tmp object |
 --------------        ------------ 

When you have a vector of pointers:

std::vector<Tmp*> vec;

and add a pointer:

vec.push_back(tmp);

then you have something like this:

 -------------- 
| variable tmp | --\
 --------------    |      ------------ 
                    >--> | Tmp object |
 --------          |      ------------ 
| vec[0] | --------/
 -------- 

From these illustrations it can easily be seen that the vector doesn't contain the Tmp object itself, only a pointer to it.

Therefore when you remove the pointer from the vector

vec.pop_back();

only the pointer inside the vector is removed and destructed. The Tmp object itself still lives on, and we once again have the first illustration.

CodePudding user response:

The destructor of int* does nothing. The destructor of T* does nothing.

You might think "destroy an int pointer" means to call delete ptr, but that isn't destroying the pointer. That is destroying what the pointer points to, and recycling the memory allocated for it (which is 2 distinct steps).

So a vector<int*> destroys all of the int*s in it; that destruction is, however, a noop.

The destructor of vector<shared_ptr<T>> also destroys all of the shared_ptr<T> within it; that destructor decrements a reference count, and if it hits zero destroys the T.

Same thing for vector<T> and vector<T*> -- in both cases, the destructor is (logically) run, but the destructor of T* is a noop, while the destructor of T is T::~T().


In C , every instance is an object. An int is an object, a int* is an object, a vector<int> is an object, and struct Foo{}; Foo foo; is an object.

Destroying an object is sometimes a noop. This is true of all primitive types, including pointers.

Because it is a noop, people get sloppy and talk about destroying a pointer the same as if they talk about destroying what is pointed to by the pointer. But they aren't the same thing.

struct Noisy {
  ~Noisy() { std::cout << "~Noisy\n"; }
};
using Ptr = Noisy*;

I can do this:

Ptr ptr;
ptr.~Ptr(); // compilers might complain here in non-generic code

and that is a noop; here I am attempting to manually call the (pseudo) destructor of ptr, which is a pointer. This (pseudo) destructor is a noop, so no code is run.

Ptr ptr = new Noisy();
delete ptr;

this actually destroys *ptr, not ptr. It then recycles the memory we used to store *ptr.

new Noisy() also does two things -- it gets memory to store a Noisy from the "free store", and then it constructs that object in the memory it got.

You can split these operations up. You can allocate the storage separately from creating the object (this is called placement new), and you can destroy the object separately from recycling the storage.

Doing so is considered an advanced technique in C , which is why nobody talks to you about it.

void demo() {
  alignas(Noisy) char buffer[sizeof(Noisy)];
  Noisy* ptr = ::new( (void*)buffer ) Noisy();
  ptr->~Noisy();
}

this makes a buffer on the stack (not automatic storage), manually constructs a Noisy object in it, then manually destroys the Noisy object.

template<class T>
void demo2() {
  alignas(T) char buffer[sizeof(T)];
  T* ptr = ::new( (void*)buffer ) T();
  ptr->~T();
}

this makes the demo generic. I can do demo<int>() or demo<Noisy>() and demonstrate construction/destruction separate from storage.

std::vector is doing something like this -- it manages a buffer of storage (measured by vec.capacity()) and a bunch of objects "at the front" of that buffer (measured by vec.size()). And it manually constructs and destroys the objects within its buffer using techniques similar to demo2.

And destroying a raw pointer doesn't cause the pointed-to object to be destroyed. But the same isn't true of an instance or a smart pointer.

  • Related