デストラクタを呼ばずに再構築

例えばこういうの。

void* p = new char[sizeof(int)];
int* pn = new (p) int(0);
pn = new (p) int(42); // デストラクタを呼ばずに再構築

int の場合は POD 型なので問題ないことは分かりますが、例えばこんな場合はどうなるか。

struct hoge {
    ~hoge() { }
};

void* p = new char[sizeof(hoge)];
int* pn = new (p) hoge();
pn = new (p) hoge(); // デストラクタを呼ばずに再構築

何となく問題が無さそうに見えますが、hoge は non-trivial なデストラクタを持っているので POD 型ではないです。
どこまでデストラクタを呼ばないのが許されるのか気になったので、調べてみました。


するとこんな記述が。

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.

N3035 §3.8¶4

プログラムは、オブジェクト領域の再利用、または non-trivial なデストラクタを持つクラス型のオブジェクトの明示的なデストラクタ呼び出しによって、どんなオブジェクトのライフタイムも終了したとみなして構わない。non-trivial なデストラクタを持つクラス型のオブジェクトのために、プログラムはオブジェクト領域が再利用・解放される前に明示的なデストラクタ呼び出しを要求しない。しかし、明示的なデストラクタ呼び出しが無いか、delete-expression が領域を解放するために使用されない場合、デストラクタが暗黙に呼び出されることは無く、デストラクタによって生じる副作用に依存している任意のプログラムは未定義とする。

ということらしく、non-trivial なデストラクタを持っていても副作用さえなければ大丈夫っぽいです。


意外と緩いんだなーと驚きでした。



しかしそのすぐ下にある§3.8¶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. 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).
#include <cstdlib>

struct B {
  virtual void f();
  void mutate();
  virtual ~B();
};

struct D1 : B { void f(); };
struct D2 : B { void f(); };

void B::mutate() {
  new (this) D2; // reuses storage ― ends the lifetime of *this
  f(); // undefined behavior
  ... = this; // OK, this points to valid memory
}

void g() {
  void* p = std::malloc(sizeof(D1) + sizeof(D2));
  B* pb = new (p) D1;
  pb->mutate();
  &pb; // OK: pb points to valid memory
  void* q = pb; // OK: pb points to valid memory
  pb->f(); // undefined behavior, lifetime of *pb has ended
}
N3035 §3.8¶5

オブジェクトのライフタイムが終了してて、再利用される前の状態でメンバとかにアクセスしたらダメだよーとか言ってるっぽいのですが、そのサンプルでは新しく D2 オブジェクト作ってるっぽいんですよね。
D2 を作ってる場所が D1 のデストラクタ呼び出しだったら分かるのですが……。うーん。