コンストラクタとデストラクタの謎の挙動まとめ
完璧に余計なお節介ですが、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.