Is It Ok Not to Call the Destructor on Placement New Allocated Objects

Is it OK not to call the destructor on placement new allocated objects?

The standard has a rule in section 3.8 [basic.life] that covers this:

A program may end the lifetime of any object by reusing the storage which the object occupies or by explicitly calling the destructor for an object of a class type with a non-trivial destructor. For an object of a class type with a non-trivial destructor, the program is not required to call the destructor explicitly before the storage which the object occupies is reused or released; however, if there is no explicit call to the destructor or if a delete-expression (5.3.5) is not used to release the storage, the destructor shall not be implicitly called and any program that depends on the side effects produced by the destructor has undefined behavior.

Lots of experts are in agreement that "depends on the side effects produced by the destructor" is far too vague to be useful. Many interpret it as a tautology meaning "If the program has undefined behavior when the destructor side effects are not evaluated, then failing to call the destructor causes undefined behavior". See Observable behavior and undefined behavior -- What happens if I don't call a destructor?

If your type has a trivial destructor (which appears to be the case in your example), then calling it (or failing to call it) has no effect whatsoever -- calling a trivial destructor does not even end the life of the object.

The lifetime of an object o of type T ends when:

  • if T is a class type with a non-trivial destructor, the destructor call starts, or
  • the storage which the object occupies is released, or is reused by an object that is not nested within o.

That is, if T doesn't have a non-trivial destructor, the only way to end the lifetime of object o is to release or reuse its storage.

Is it dangerous to use placement new on an old object without explicitly calling the destructor first?

No, it is not dangerous to reuse memory of an object, provided that you are doing it correctly. Moreover, you do not have to restrict yourself to objects that have no pointers: by calling the destructor explicitly you can prepare the object for reuse, like this:

Foo* foo = new Foo;
// Do something with foo
// Done with foo, writing a new version of foo on top of the old one.
foo->~Foo(); // Call the destructor explicitly to clean up the resources of a Foo
new(foo) Foo(); // Place new data into the previously allocated memory
delete(foo); // We are deleting a fully initialized object, so it is OK

Does placement-new into the same type still require to manually call the destructor?

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)];

Is it allowed to call destructor explicitly followed by placement new on a variable with fixed lifetime?

First, [basic.life]/8 clearly states that any pointers or references to the original foo shall refer to the new object you construct at foo in your case. In addition, the name foo will refer to the new object constructed there (also [basic.life]/8).

Second, you must ensure that there is an object of the original type the storage used for foo before exiting its scope; so if anything throws, you must catch it and terminate your program ([basic.life]/9).

Overall, this idea is often tempting, but almost always a horrible idea.

  • (8) If, after the lifetime of an object has ended and before the storage which the object occupied is reused or released, a new object is created at the storage location which the original object occupied, a pointer that pointed to the original object, a reference that referred to the original object, or the name of the original object will automatically refer to the new object and, once the lifetime of the new object has started, can be used to manipulate the new object, if:

    • (8.1) the storage for the new object exactly overlays the storage location which the original object occupied, and
    • (8.2) the new object is of the same type as the original object (ignoring the top-level cv-qualifiers), and
    • (8.3) the type of the original object is not const-qualified, and, if a class type, does not contain any non-static
      data member whose type is const-qualified or a reference type, and
    • (8.4) the original object was a most derived object (1.8) of type
      T and the new object is a most derived
      object of type T (that is, they are not base class subobjects).
  • (9) If a program ends the lifetime of an object of type T with static (3.7.1), thread (3.7.2), or automatic (3.7.3) storage duration and if T has a non-trivial destructor, the program must ensure that an object of the
    original type occupies that same storage location when the implicit destructor call takes place; otherwise the behavior of the program is undefined. This is true even if the block is exited with an exception.

There are reasons to manually run destructors and do placement new. Something as simple as operator= is not one of them, unless you are writing your own variant/any/vector or similar type.

If you really, really want to reassign an object, find a std::optional implementation, and create/destroy objects using that; it is careful, and you almost certainly won't be careful enough.

Does C++ require a destructor call for each placement new?

If I understand your question correctly you have a C object in memory and you want to initialize a C++ object with the same layout "over the top" of the existing object.

CppObject* cppobject = new (cobject) CppObject;

While there is no problem with not calling a destructor for the old object - whether this causes resource leaks or other issues is entirely down to the type of the old object and is a user code issue, not a language conformance issue - the fact that you reuse the memory for a new object means that the old object is no longer accessible.

Although the placement form of operator new must just return the address that it was given, there is nothing to stop the new expression itself wiping the memory for the new object before any constructor (if any) is called. Members of the new object that are not initialized according to C++ language rules have unspecified contents which definitely does not mean the same as having the contents of any old object that once lived in the memory being reused.

If I understand you correctly, what you are trying to do is not guaranteed to work.

How to properly free the memory allocated by placement new?

Using the new expression does two things, it calls the function operator new which allocates memory, and then it uses placement new, to create the object in that memory. The delete expression calls the object's destructor, and then calls operator delete. Yeah, the names are confusing.

//normal version                   calls these two functions
MyClass* pMemory = new MyClass; void* pMemory = operator new(sizeof(MyClass));
MyClass* pMyClass = new( pMemory ) MyClass();
//normal version calls these two functions
delete pMemory; pMyClass->~MyClass();
operator delete(pMemory);

Since in your case, you used placement new manually, you also need to call the destructor manually. Since you allocated the memory manually, you need to release it manually.

However, placement new is designed to work with internal buffers as well (and other scenarios), where the buffers were not allocated with operator new, which is why you shouldn't call operator delete on them.

#include <type_traits>

struct buffer_struct {
std::aligned_storage_t<sizeof(MyClass), alignof(MyClass)> buffer;
};
int main() {
buffer_struct a;
MyClass* pMyClass = new (&a.buffer) MyClass(); //created inside buffer_struct a
//stuff
pMyClass->~MyClass(); //can't use delete, because there's no `new`.
return 0;
}

The purpose of the buffer_struct class is to create and destroy the storage in whatever way, while main takes care of the construction/destruction of MyClass, note how the two are (almost*) completely separate from each other.

*we have to be sure the storage has to be big enough

Is it safe to call placement new on `this` for trivial object?

Similarly to the legality of delete this, placement new to this is also allowed as far as I know. Also, regarding whether this, or other pre-existing pointers / references can be used afterwards, there are a few restrictions:

[basic.life]

If, after the lifetime of an object has ended and before the storage which the object occupied is reused or released, a new object is created at the storage location which the original object occupied, a pointer that pointed to the original object, a reference that referred to the original object, or the name of the original object will automatically refer to the new object and, once the lifetime of the new object has started, can be used to manipulate the new object, if:

  • the storage for the new object exactly overlays the storage location which the original object occupied, and
  • the new object is of the same type as the original object (ignoring the top-level cv-qualifiers), and
  • the type of the original object is not const-qualified, and, if a class type, does not contain any non-static data member whose type is
    const-qualified or a reference type, and
  • neither the original object nor the new object is a potentially-overlapping subobject ([intro.object]).

The first two are satisfied in this example, but the last two will need to be taken into consideration.

Regarding the third point, given that the function is non-const-qualified, it should be fairly safe to assume that the original object is non-const. The fault is on the caller side if the constness has been cast away. Regarding const / reference member, I think that can be checked by asserting that this is assignable:

static_assert(std::is_trivial_v<A> && std::is_copy_assignable_v<A>);

Of course, since assignability is a requirement, you could instead simply use *this = {}; which I would expect to produce the same program. A perhaps more interesting use case might be to reuse memory of *this for an object of another type (which would fail the requirements for using this, at least without reinterpreting + laundering).

Similar to delete this, placement new to this could hardly be described as "safe".



Related Topics



Leave a reply



Submit