Home > Software design >  Does placement-new into the same type still require to manually call the destructor?
Does placement-new into the same type still require to manually call the destructor?

Time:06-02

Context

I'm trying to get a grasp on the placement-new mechanism since I never have had to use it. I'm trying to understand how to properly use it out of pure curiosity.

For the question, we will consider the following code base for illustration purposes:

struct Pack
{
    int a, b, c, d;
    Pack() = default;
    Pack(int w, int x, int y, int z) : a(w), b(x), c(y), d(z)
    {}
    ~Pack()
    {
        std::cout << "Destroyed\n";
    }
};

std::ostream & operator<<(std::ostream & os, const Pack & p)
{
    os << '[' << p.a << ", " << p.b << ", " << p.c << ", " << p.d << ']';
    return os;
}

Basically it just defines a structure holding some data (4 integers in the example), and I overloaded the operator<<() to simplify the code in the testing part.

I know that when using placement-new, one has to manually call the destructor of the object because we can't use delete since we only want to destroy the object (and not to free the memory since it was already allocated).

Example (A):

char b[sizeof(Pack)];
Pack * p = new (b) Pack(1, 2, 3, 4);
std::cout << *reinterpret_cast<Pack*>(b) << '\n';

p->~Pack();

Question

I was wondering, when using placement-new into an object of the same type, is it still necessary to call the destructor of the object ?

Example (B):

Pack p;
new (&p) Pack(1, 2, 3, 4);
std::cout << p << '\n';

I did a quick test of both version here and it seems that the destructor is properly called when the underlying object goes out of scope while being of type Pack.
I know it's stupid to use placement-new in such a trivial example instead of directly creating the object. But in a real-case example, it can be a buffer or Pack instead of a buffer of char.

I would like to know if I am right to assume that I don't need to manually call the destructor in the example (B) or if I'm missing something.


Extra

As a subsidiary question, in the example (A), is it legal to get rid of the returned pointer and call the destructor through the reinterpret_casted pointer instead ?

For example can it be rewritten like this:

char b[sizeof(Pack)];
new (b) Pack(1, 2, 3, 4);
std::cout << *reinterpret_cast<Pack*>(b) << '\n';

reinterpret_cast<Pack*>(b)->~Pack(); // Is it legal ?

It would be useful in the case I have a buffer of objects and I want to destroy its elements without having to keep the returned pointers somewhere.

[Disclaimer]: Of course, in a real case program, I would use a std::vector and the emplace_back() function instead of my own buffer. As already mentioned, this question is purely out of curiosity, just to understand how it's meant to work under the hood.

CodePudding user response:

Yes, you must call the destructor. It's irrelevant whether the old object is of same type or not.

Only thing that matters is whether the type of the old object is trivially destructible. If it is, then there is no need to call the destructor. If it isn't, then you must call the destructor before reusing the memory.

Example:

Pack p;

new (&p) Pack(1, 2, 3, 4); // Not OK

p.~Pack();
new (&p) Pack(1, 2, 3, 4); // OK

Note that there are cases where this isn't allowed such as if the class contains const qualified members or reference members. In general, I recommend avoiding such pattern, and to instead re-use only arrays of char or similar trivial storage.

As a subsidiary question, in the example (A), is it legal to get rid of the returned pointer and call the destructor through the reinterpret_casted pointer instead ?

Just like all uses of the placement-newed object, you can reintepret the addresss of the original object, but you must launder it:

Pack* ptr = new (b) Pack(1, 2, 3, 4);

std::cout << *ptr << '\n'; // OK
std::cout << *reinterpret_cast<Pack*>(b) << '\n'; // Not OK
std::cout << *std::launder(reinterpret_cast<Pack*>(b)) << '\n'; // OK

ptr->~Pack(); // OK
reinterpret_cast<Pack*>(b)->~Pack(); // Not OK
std::launder(reinterpret_cast<Pack*>(b))->~Pack(); // OK

char b[sizeof(Pack)];
Pack * p = new (b) Pack(1, 2, 3, 4);

This is wrong. You must ensure that the storage is properly aligned for the placement-newed type:

alignas(alignof(Pack)) char b[sizeof(Pack)];

CodePudding user response:

Not calling a destructor is not undefined behaviour. But it could lead to undefined behaviour if you rely on the side effects of the destructor.

In your example (a), there are two Pack objects created. One is default constructed at Pack p;. The lifetime of this object ends when you reuse its storage with new (b) Pack(1, 2, 3, 4);, starting the lifetime of a different Pack object at the same address, which ends with an explicit call to the destructor. Note that there are 2 constructors called but only 1 destructor. If this class held some resources (like a std::vector or std::string), it could lead to a memory leak, but that is not UB in itself.

If the destructor was trivial, there are no side effects, so skipping the destructor call will always be OK.

  • Related