2021年3月6日

const修飾されたインタフェース型の引数としてその場で生成したインスタンスをインタフェース型にキャストしないでそのまま渡すとリークする

Dalija PrasnikarさんのDelphi Event-based and Asynchronous Programmingをパラ見していて引っかかったのですが、タイトルを見ても何をいっているのかわからないと思うので、まずコードを。
type
  IFoo = interface
    procedure FooBar;
  end;

  TFoo = class(TInterfacedObject,IFoo)
  public
    procedure FooBar;
  end;

  TBar = class(TObject)
  public
    procedure Baz(const AFoo: IFoo);
  end;

procedure TFoo.FooBar;
begin
// Do something.
end;

procedure TBar.Baz(const AFoo: IFoo);
begin
  AFoo.FooBar;
end;
インタフェースとしてIFooと、その実装としてクラスTFooを用意し、クラスTBarにはconst修飾されたIFooを渡すBazというメソッドがあります。ここで
var
  Bar: TBar;
begin
  Bar := TBar.Create;
  Bar.Baz(TFoo.Create);
  Bar.Free;
end;
とTBar.BazにTFoo.Createで生成したインスタンスを直接渡すと、TFooはIFooとしての参照カウントの制御を受けず、リークしてしまいます。一方で
var
  Bar: TBar;
begin
  Bar := TBar.Create;
  Bar.Baz(TFoo.Create as IFoo);
  Bar.Free;
end;
とIFooにキャストしたものを渡すとリークしなくなります。

これは、インタフェース型の引数がconst修飾されているとメソッド内部で仮引数が変更されないことが保証されているため、仮引数にコピーしたことによる参照カウントの管理を行わない最適化が行われるのに対して、前者のコードでは呼び出し元で生成したインスタンスが(インタフェース型ではなく)クラス型としてしか管理されていないため、やはり参照カウントの管理を受けずに、リークしてしまう、ということのようです。しかし後者ではas IFooとキャストしたことでIFoo型の暗黙のローカル変数が用意され、これによって参照カウントによって正常に解放されます。

メソッド内部で仮引数を別のインタフェース型の変数やフィールドにコピーすると参照カウントの管理が行われて適切に解放が行われますが、これは実装に依存しますし、また引数をconst修飾していなければ大丈夫ですが、既存の実装の変更が必要になる、ということが問題になります。
このためconst修飾されたインタフェース型の引数としてその場で生成したインスタンスを渡す場合はインタフェース型にキャストするか、あるいは
type
  TFoo = class(TInterfacedObject,IFoo)
  public
    class function CreateAsIntf: IFoo;
    procedure FooBar;
  end;

class function TFoo.CreateAsIntf: IFoo;
begin
  Result := TFoo.Create;
end;

var
  Bar: TBar;
begin
  Bar := TBar.Create;
  Bar.Baz(TFoo.CreateAsIntf);
  Bar.Free;
end;
このように生成したインスタンスをインタフェースとして返すようなメソッドを用意して、そちらを経由するか、ということになります。

元ねたはもちろんDalija PrasnikarさんのDelphi Event-based and Asynchronous Programmingの"13.4 In-place construction in a const parameter"。

0 件のコメント: