user3663882
user3663882

Reputation: 7357

Storage reuse in C++

I have been trying to understand storage reuse in C++. Imagine we have an object a with a non-trivial destructor whose storage is reused with a placement new-expression:

struct A {
    ~A() { std::cout << "~A()" << std::endl; }
};    
struct B: A {};
A* a = new A;     // lifetime of *a begins
A* b = new(a) B;  // storage reuse, lifetime of *b begins

[basic.life/8] specifies:

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 original object is transparently replaceable (see below) by the new object.

Since in my example the lifetime of *a has not ended when we reuse the storage it occupies, we cannot apply that rule. So what rule describes the behavior in my case?

Upvotes: 3

Views: 543

Answers (3)

T.C.
T.C.

Reputation: 137320

The applicable rule for this is laid out in §3.8 [basic.life]/p1 and 4:

The lifetime of an object of type T ends when:

  • if T is a class type with a non-trivial destructor (12.4), the destructor call starts, or
  • the storage which the object occupies is reused or released.

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

So A *b = new (a) B; reuses the storage of the A object created in the previous statement, which is well-defined behavior provided that sizeof(A) >= sizeof(B)*. That A object's lifetime has ended by virtue of its storage being reused. A's destructor is not called for that object, and if your program depends on the side effect produced by that destructor, it has undefined behavior.

The paragraph you cited, §3.8 [basic.life]/p7, governs when a pointer/reference to the original object can be reused. Since this code doesn't satisfy the criteria listed in that paragraph, you may only use a only in the limited ways permitted by §3.8 [basic.life]/p5-6, or undefined behavior results (example and footnote omitted):

5 Before the lifetime of an object has started but after the storage which the object will occupy has been allocated or, after the lifetime of an object has ended and before the storage which the object occupied is reused or released, any pointer that refers to the storage location where the object will be or was located may be used but only in limited ways. For an object under construction or destruction, see 12.7. Otherwise, such a pointer refers to allocated storage (3.7.4.2), and using the pointer as if the pointer were of type void*, is well-defined. Such a pointer may be dereferenced but the resulting lvalue may only be used in limited ways, as described below. The program has undefined behavior if:

  • the object will be or was of a class type with a non-trivial destructor and the pointer is used as the operand of a delete-expression,
  • the pointer is used to access a non-static data member or call a non-static member function of the object, or
  • the pointer is implicitly converted (4.10) to a pointer to a base class type, or
  • the pointer is used as the operand of a static_cast (5.2.9) (except when the conversion is to void*, or to void* and subsequently to char*, or unsigned char*), or
  • the pointer is used as the operand of a dynamic_cast (5.2.7).

6 Similarly, before the lifetime of an object has started but after the storage which the object will occupy has been allocated or, after the lifetime of an object has ended and before the storage which the object occupied is reused or released, any glvalue that refers to the original object may be used but only in limited ways. For an object under construction or destruction, see 12.7. Otherwise, such a glvalue refers to allocated storage (3.7.4.2), and using the properties of the glvalue that do not depend on its value is well-defined. The program has undefined behavior if:

  • an lvalue-to-rvalue conversion (4.1) is applied to such a glvalue,
  • the glvalue is used to access a non-static data member or call a non-static member function of the object, or
  • the glvalue is implicitly converted (4.10) to a reference to a base class type, or
  • the glvalue is used as the operand of a static_cast (5.2.9) except when the conversion is ultimately to cv char& or cv unsigned char&, or
  • the glvalue is used as the operand of a dynamic_cast (5.2.7) or as the operand of typeid.

* To prevent UB from cases where sizeof(B) > sizeof(A), we can rewrite A *a = new A; as char c[sizeof(A) + sizeof(B)]; A* a = new (c) A;.

Upvotes: 5

WaelJ
WaelJ

Reputation: 3012

As an addendum, in order to do this correctly you may call the destructor explicitly.

Note: the located memory is of size B to accommodate the potential size between A and B.

Note 2: with your implementation of class A this will not work. ~A() must be made virtual!!

A *b = new B; //Lifetime of b is starting. It is important that we use `new B` rather than `new A` so as to get the correct size.
b->~B(); //lifetime of b has ended. The memory still remain allocated however.
A *a = new (a) A; //lifetime of a is starting
a->~A(); // lifetime of a has ended

// a is still allocated but in an undefined state

::operator delete(b); // release the memory allocated without calling the destructor. This is different from calling 'delete b'

I believe that calling operator delete on a base pointer should be safe. Please do correct me if this is not the case.

Alternatively, if you allocate the memory for a as a char buffer, you can then use placement new to construct A and B objects, and safely call delete[] to deallocate the buffer (since char has a trivial destructor):

char* buf = new char[sizeof(B)];
A *a = new (a) A;
a->~();
A *b = new (a) B;
b->~B();
delete[] buf;

Upvotes: 0

Mats Petersson
Mats Petersson

Reputation: 129344

There are some potential problems with this:

  1. If B is larger than A, it will overwrite bytes not allocated - which is undefined behaviour.
  2. Destructor of A is not called for a (or b - your code doesn't show whether you delete a or delete b or neither). This is very important if either for A or B destructor is doing something like reference counting, locks, memory deallocation (including std:: containers such as std::vector or std::string), etc.

If a is not used again after you create b, you still need to call the A destructor to make sure it's lifetime is over - see the example in the third bulled after the section you quoted. So if your purpose was to avoid the "expensive" destructor call, then your code is failing to abide by the rules given in section 3.8/7 of the standard.

You are also breaching the bullet of:

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

as A is not the most derived type.

In summary, "broken". Even in cases where it does work (e.g. changing to A* a = new B;), it should be discouraged, as it can lead to subtle and difficult bugs.

Upvotes: 1

Related Questions