2011年5月9日

コンストラクタとデストラクタについての考察

Delphiのコンストラクタ、デストラクタとは、ヘルプの

コンストラクタ (メソッド)
デストラクタ (メソッド)

にあるように、クラスのインスタンスを生成、破棄するための特別なメソッドです。あまり知られていませんがコンストラクタとデストラクタには

メソッド呼び出しの処理 (プログラムの制御)

にあるように、Pointer型とByte型の2つの隠しパラメータが存在します(デフォルトのregister呼出規約ではEAXレジスタとDLレジスタ)。第1パラメータ(Pointer型)はSelf(C++のthisに相当)です。一方で第2パラメータ(Byte型)は真偽値(0または非0)であり、クラスメソッドとして呼び出される場合、つまり
  Foo := TFoo.Create;

では1(真)が設定され、インスタンスメソッドとして呼び出される場合、つまり
constructor TFoo.Create;
begin
  inherited;
end;

のinherited(継承元コンストラクタの呼び出し)では0(偽)が設定されます。コンストラクタでは先頭にこの第2パラメータをチェックして真の場合はインスタンス領域の確保と初期化が行われるようなコードが生成されます(確保したインスタンス領域のアドレスはEAXに格納され、呼出元に返されます)。またデストラクタでは末尾に第2パラメータのチェックとインスタンス領域の解放が行われるようなコードが生成されます。さらにコンストラクタでは内部で例外が生成されたときに自動的にデストラクタが呼び出され、初期化後に変更されたフィールドに対してリソースの解放を行うことでコンストラクタで例外を送出したときのリソースリークを防ぐような仕組みになっています。

実際のコードで確認してみましょう(例はシングルトンパターンその1のUnit2をDelphi 2007でデバッグビルドしたものです)。

コンストラクタ呼び出し
  FSingleton := TSingleton.Create;



Unit2.pas.30: FSingleton := TSingleton.Create;
00457C35 B201             mov dl,$01
00457C37 A1D47B4500       mov eax,[$00457bd4]
00457C3C E80B000000       call TSingleton.Create
00457C41 A3CC0C4600       mov [$00460ccc],eax

となり、DLレジスタに0x01が設定されてコンストラクタ(TSingleton.Create)を呼び出しています。コンストラクタ本体の
constructor TSingleton.Create;
begin

  inherited;

  { Initialize }
  FTestValue := 0;

end;



Unit2.pas.38: begin
00457C4C 53               push ebx
00457C4D 56               push esi
00457C4E 84D2             test dl,dl
00457C50 7408             jz $00457c5a
00457C52 83C4F0           add esp,-$10
00457C55 E8B2C0FAFF       call @ClassCreate
00457C5A 8BDA             mov ebx,edx
00457C5C 8BF0             mov esi,eax
Unit2.pas.40: inherited;
00457C5E 33D2             xor edx,edx
00457C60 8BC6             mov eax,esi
00457C62 E869BDFAFF       call TObject.Create
Unit2.pas.43: FTestValue := 0;
00457C67 33C0             xor eax,eax
00457C69 894604           mov [esi+$04],eax
Unit2.pas.45: end;
00457C6C 8BC6             mov eax,esi
00457C6E 84DB             test bl,bl
00457C70 740F             jz $00457c81
00457C72 E8EDC0FAFF       call @AfterConstruction
00457C77 648F0500000000   pop dword ptr fs:[$00000000]
00457C7E 83C40C           add esp,$0c
00457C81 8BC6             mov eax,esi
00457C83 5E               pop esi
00457C84 5B               pop ebx
00457C85 C3               ret

と先頭でDLレジスタが非0ならばClassCreateを呼び出し、BLレジスタにDLレジスタの値を保存してからDLレジスタを0に変更して以下の処理を行い、最後にBLレジスタ(=呼び出されたときのDLレジスタ)が非0であればAfterConstructionを呼び出すようになっています。

次にデストラクタを見てみます。といってもサンプルではTObject.Freeで呼び出しが隠蔽されてしまっていますので、ここはDestroyを直接呼び出すようにしてみます。
  FSingleton.Destroy;



Unit2.pas.63: FSingleton.Destroy;
00457CD2 B201             mov dl,$01
00457CD4 A1CC0C4600       mov eax,[$00460ccc]
00457CD9 8B08             mov ecx,[eax]
00457CDB FF51FC           call dword ptr [ecx-$04]

となり、やはりDLレジスタに0x01を格納してからデストラクタを呼び出しています(呼出先が[ecx-$04]というのはVMT経由でデストラクタのアドレスを取得しているためです)。デストラクタ本体の
destructor TSingleton.Destroy;
begin

  { Finalize }

  inherited;

end;



Unit2.pas.48: begin
00457C88 53               push ebx
00457C89 56               push esi
00457C8A E825C1FAFF       call @BeforeDestruction
00457C8F 8BDA             mov ebx,edx
00457C91 8BF0             mov esi,eax
Unit2.pas.52: inherited;
00457C93 8BD3             mov edx,ebx
00457C95 80E2FC           and dl,$fc
00457C98 8BC6             mov eax,esi
00457C9A E851BDFAFF       call TObject.Destroy
Unit2.pas.54: end;
00457C9F 84DB             test bl,bl
00457CA1 7E07             jle $00457caa
00457CA3 8BC6             mov eax,esi
00457CA5 E8B2C0FAFF       call @ClassDestroy
00457CAA 5E               pop esi
00457CAB 5B               pop ebx
00457CAC C3               ret

となり、まずBeforeDestructionが呼び出され、BLレジスタにDLレジスタの値を保存してから0xFCとANDをとって(DLレジスタは0x01 AND 0xFC = 0x00となっている)継承元デストラクタを呼び出し、最後にBLレジスタ(呼び出されたときのDLレジスタ)が非0であればClassDestroyを呼び出すようになっています。またデストラクタ内でExitしても

Unit2.pas.48: begin
00457C88 53               push ebx
00457C89 56               push esi
00457C8A E825C1FAFF       call @BeforeDestruction
00457C8F 8BDA             mov ebx,edx
00457C91 8BF0             mov esi,eax
Unit2.pas.50: Exit;
00457C93 EB0C             jmp $00457ca1
Unit2.pas.54: inherited;
00457C95 8BD3             mov edx,ebx
00457C97 80E2FC           and dl,$fc
00457C9A 8BC6             mov eax,esi
00457C9C E84FBDFAFF       call TObject.Destroy
Unit2.pas.56: end;
00457CA1 84DB             test bl,bl
00457CA3 7E07             jle $00457cac
00457CA5 8BC6             mov eax,esi
00457CA7 E8B0C0FAFF       call @ClassDestroy
00457CAC 5E               pop esi
00457CAD 5B               pop ebx
00457CAE C3               ret

とBLレジスタのチェックにジャンプするようになっており、DLが非0の呼び出しでは途中でExitしても必ずClassDestroyが呼び出されます。

このような事情から、コンストラクタの呼び出しによるクラスインスタンスの生成をキャンセルするためにはコンストラクタ内で例外を送出し、最外側コンストラクタ先頭で確保したインスタンス領域をデストラクタで解放させるしかありません(コンストラクタをクラスメソッドで呼び出すと必ず領域確保が行われるため)。同様にデストラクタの呼び出しによるインスタンスの破棄をキャンセルするには最外側のデストラクタの末尾までのどこかの時点で必ず例外を送出してインスタンス領域の解放を防ぐ必要があります。

元ねたはDelphiクイックリファレンス (amazon)/Ray Lischner著/光田秀、竹田知生訳/オライリー・ジャパン/ISBN4-87311-040-8/4,725円。

0 件のコメント: