コンストラクタとデストラクタの謎の挙動まとめ

完璧に余計なお節介ですが、id:mole-studioさんが迷っておられてます*1ので、死んだ知識ではありますが。ちなみにDelphi7ぐらいの知識で書いてますので最近のことはわかりません!

Delphiのconstructorは、外からみるとクラスメソッド、中からみるとインスタンスメソッドな謎関数です。
細かく言えば、型名.Createやクラス参照.Createで呼んだときはクラスメソッド、インスタンス.Createやinherited Createとして呼んだときはインスタンスメソッドです。前者の時はメモリの割当や初期化処理を行い、後者の時は単なるメソッドです。

これがどうやって実現されているか。
答えを書いてしまいますと、単に隠しパラメータがあるのです。それだけです。(ヘルプのインラインアセンブラ回りに書いてたような書いてなかったような)

cosntructor T.Create;
begin
  inherited Create;
  DoSomething;
end;

↑のいつものコンストラクタは、実はこうなっています。

procedure T.Create (Flag: Boolean); (* クラスメソッドの時はTrue *)
begin
  if Flag then
    Self := NewInstance; (* ここまでSelfにはクラス参照が入っているためvirtualなクラスメソッドのNewInstanceを呼べる *)
  end if;
  inherited Create (False);
  DoSomething;
  if Flag then
    AfterConstruction;
  end if;
end;

デストラクタも同じ。デストラクタはクラスメソッドにはなりませんが、最初に呼ばれたときとinheritedで呼ばれたときでは挙動が異なります。

destructor T.Destroy;
begin
  DoSomething;
  inherited Destroy;
end;

procedure T.Destroy (Flag: Boolean); (* 普通に呼ばれたらTrue *)
begin
  if Flag then
    BeforeDestruction;
  end if;
  DoSomething;
  inherited Destroy (False);
  if Flag then
    FreeInstance;
  end if;
end;

BeforeDestruction/AfterConstructionは単なるvirtualなインスタンスメソッドに過ぎず、Self書き換えを行っている場合、到達時点でVMTが設定されていないと落ちます。(勿論constructor自体をvirtualにすることもありますので、NewInstanceでダミー値を返してCreate中で実際のメモリを確保なんてのはやめといたほうがいいでしょう。といってもNewInstanceは引数がありませんので、パラメータを渡したいときはthreadvar(TLS)が適当かと思います。)
この展開がありますので、実はDestroyをoverrideするよりもBeforeDestructionをoverrideしたほうが生成コードサイズが少し減ります!(キリッ)……でもBeforeDestructionは外から何か悪さもといフック仕掛けるときに便利なポイントですので、フックを仕掛けられるのを前提に終了処理は普通にDestroyに書いておいたほうがいいかもしれません。

NewInstance(普通のvirtualクラスメソッド)は単にGetMem→InitInstance、FreeInstance(普通のvirtualインスタンスメソッド)は単にCleanupInstance→FreeMemで、こっちもoverrideして挙動を変えることが出来ます。
InitInstanceは、メモリをゼロクリアして先頭にVMT*2を設定するだけです。Delphiの型で暗黙の初期化が必要なのは文字列系、バリアント系、interfaceぐらいで、これら全部の型でゼロが中身からっぽとして扱われますので、手が抜かれています。(recordをローカル変数に確保したときは真面目にRTTI(2009以降のラッパーではない旧式な方)を見て必要なところだけ初期化されます)
CleanupInstanceのほうはそういうわけにもいきませんので、真面目にRTTI(旧式ry)を見てきちんと開放されます。
いずれにせよ自力での実装はそれなりに手間ですのでInitInstance/CleanupInstnaceには手をつけずにきちんと呼んであげたほうがいいです。

これらの挙動を押さえておけば、俺メモリマネージャを簡単に適用できるはずです。

type
  IMyMemoryManager = interface (* 俺メモリマネージャ用インターフェース *)
     function Alloc(Size : Cardinal): Pointer;
     procedure Free(P: Pointer);
  end;

type
  TExtMem = class(TObject) (* 外部メモリを使うクラス *)
  private
     FMemMan : IMyMemoryManager; (* 確実に確保時と同じものを使えるように念のため保存 *)
  public
     class function NewInstance: TObject; override;
     procedure FreeInstance; override;
  end;

threadvar
  MemMan: IMyMemoryManager; (* このメモリマネージャを使って! *)

class function TExtMem.NewInstance: TObject;
begin
  Result := InitInstance(MemMan.Alloc(InstanceSize));
  (Result as TExtMem).FMemMan := MemMan; (* 確保時のメモリマネージャを覚えとく *)
end;

procedure TExtMem.FreeInstance;
var
  M: IMyMemoryManager;
begin
  M := FMemMan; (* クリアから退避 *)
  CleanupInstance;
  M.Free(Self);
end;

var
  PObj: TExtMem;
begin
  //MemMan := ... (* 俺メモリマネージャを設定 *)
  PObj := TExtMem.Create;
  PObj.Free;
end.

クラスメソッドにはクラス参照が渡ってきており、派生クラスでもInstanceSizeも適切な値を返しますしInitInstanceもちゃんと動きますので、あるクラスに仕掛けたら派生クラスで気にすることはありません。

あと空気のように(* *)コメントを使ってた件。{}だろjk……Oなんとかに毒されすぎだ。

ここから下は余談。

挙動はrecordと同じでいいから継承だけ使いたい!ってときはobject型を使うと便利です。あらかじめ定義されたフィールドやメソッドは一切無く、本当にゼロからオブジェクトを作り上げることができます。純粋にVMTが持てるだけのrecordと思えばいいです。いざ自由でカオスな世界へ!
classはTurboPascal→Delphiの流れでBorlandが勝手に追加してFreePascalやらなんかが後追いしてるだけの型で、object型のほうが正当な(?)"Obect Pascal"の型なのです。どうでもいい。

type TMyRoot = object (* ←TObjectに相当するルート型は無いので自分で作る *)
  constructor Init; (* Delphi1.0に付いてきたソースではInit / Doneが使われてた *)
  destructor Done; virtual;
end;

constructor TMyRoot.Init;
begin
end;

destructor TMyRoot.Done;
begin
end;

type TMyDerived = object(TMyRoot) (* 派生 *)
private
  FProp: Integer;
public
  constructor Init(AProp: Integer);
  destructor Done; virtual; (* overrideのときもoverrideではなくvirtualと書く *)
  property Prop: Integer read FProp; (* 一部class用の構文も使える、詳細不明 *)
end;

constructor TMyDerived.Init (AProp: Integer);
begin
  inherited Init;
  FProp := AProp;
end;

destructor TMyDerived.Done;
begin
  inherited Done;
end;

type
  PMyDerived = ^TMyDerived; (* ポインタ *)

var
  Obj: TMyDerived; (* 静的に確保 *)
  P: PMyDerived; (* ポインタだけ *)
begin
  Obj.Init (10); (* 領域は存在してるがconstructorを呼ばないとVMTが設定されない *)
  Obj.Done;
  New (P, Init (20)); (* 動的に確保 *)
  Dispose (P, Done); (* 開放 *)
  New (P); (* メモリだけ確保して *)
  P^.Init (30); (* 後から初期化 *)
  P^.Done; (* 先に後始末だけして *)
  Dispose (P); (* メモリを開放 *)
  P := New (PMyDerived, Init (40)); (* こういう書き方も有り *)
  Dispose (P, Done);
end.

*1:http://d.hatena.ne.jp/mole-studio/20110317/1300356089と一連の記事参照。はてなはいつの間にidだけじゃなくて個々の記事にリンクを貼らないとトラックバックできなくなったんだ。……って、うわー、今度は何重にもトラックバックされてる……っ。ごめんなさいすみませんすみません。適当に消していただければ幸いです。

*2:そういえばTMonitor領域なんてのもあったな……無くなって欲しいな……。