Search code examples
c++language-lawyerobject-lifetimeplacement-new

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?


Solution

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