编译器在将对象实例直接作为const接口参数传递时是否应该提示/警告?

Jer*_*ers 17 delphi parameters const interface

在将对象的新实例传递给具有对象类实现的接口的const接口参数的方法时,编译器是否应该提示/警告?

编辑:当然样本很容易说明问题.但在现实生活中,它变得更加复杂:如果创建和使用在相隔很远的代码(不同的单元,不同的类,不同的项目)中会怎么样?如果由不同的人维护怎么办?如果非const参数变为常量参数,并且不能检查所有调用代码(因为更改代码的人无法访问所有调用代码),该怎么办?

下面的代码崩溃了,很难找到原因.

首先是日志:

1.Run begin

1.RunLeakCrash
 2.RunLeakCrash begin
     NewInstance 1
     AfterConstruction 0
   3.LeakCrash begin
     _AddRef 1
    4.Dump begin
    4.Dump Reference=10394576
    4.Dump end
     _Release 0
     _Release Destroy
     BeforeDestruction 0
   3.LeakCrash Reference got destroyed if it had a RefCount of 1 upon entry, so now it can be unsafe to access it
     _AddRef 1
    4.Dump begin
    4.Dump Reference=10394576
    4.Dump end
     _Release 0
     _Release Destroy
     BeforeDestruction 0
   3.LeakCrash end with exception

1.Run end
EInvalidPointer: Invalid pointer operation
Run Code Online (Sandbox Code Playgroud)

然后过早释放实现接口的对象实例的代码:

//{$define all}

program InterfaceConstParmetersAndPrematureFreeingProject;

{$APPTYPE CONSOLE}

uses
  SysUtils,
  Windows,
  MyInterfacedObjectUnit in '..\src\MyInterfacedObjectUnit.pas';

procedure Dump(Reference: IInterface);
begin
  Writeln('    4.Dump begin');
  Writeln('    4.Dump Reference=', Integer(PChar(Reference)));
  Writeln('    4.Dump end');
end;

procedure LeakCrash(const Reference: IInterface);
begin
  Writeln('   3.LeakCrash begin');
  try
    Dump(Reference); // now we leak because the caller does not keep a reference to us
    Writeln('   3.LeakCrash Reference got destroyed if it had a RefCount of 1 upon entry, so now it can be unsafe to access it');
    Dump(Reference); // we might crash here
  except
    begin
      Writeln('   3.LeakCrash end with exception');
      raise;
    end;
  end;
  Writeln('   3.LeakCrash end');
end;

procedure RunLeakCrash;
begin
  Writeln(' 2.RunLeakCrash begin');
  LeakCrash(TMyInterfacedObject.Create());
  Writeln(' 2.RunLeakCrash end');
end;

procedure Run();
begin
  try
    Writeln('1.Run begin');

    Writeln('');
    Writeln('1.RunLeakCrash');
    RunLeakCrash();

  finally
    Writeln('');
    Writeln('1.Run end');
  end;
end;

begin
  try
    Run();
  except
    on E: Exception do
      Writeln(E.ClassName, ': ', E.Message);
  end;
  Readln;
end.
Run Code Online (Sandbox Code Playgroud)

EInvalidPointer将在第二次调用中显示出来Dump(Reference);.原因是公开Reference的底层对象的引用计数已经为零,因此底层对象已经被销毁.

关于编译器插入或省略的引用计数代码的一些注意事项:

  • 未标记的参数const(如in procedure Dump(Reference: IInterface);)获取隐式try/finally块以执行引用计数.
  • 标有const(如in procedure LeakCrash(const Reference: IInterface);)的参数不会得到任何引用计数代码
  • 传递对象实例创建的结果(如LeakCrash(TMyInterfacedObject.Create());)不会生成任何引用计数代码

所有上述编译器行为都是非常合乎逻辑的,但结合它们会导致EInvalidPointer.
EInvalidPointer仅在非常狭窄的使用模式中显示.
该模式很容易被编译器识别,但很难调试或找到陷入其中的原因.
解决方法非常简单:将结果缓存到TMyInterfacedObject.Create()中间变量中,然后将其传递给LeakCrash().

编译器是否应提示或警告您此使用模式?

最后我用来跟踪所有_AddRef/_Release/etcetera调用的代码:

unit MyInterfacedObjectUnit;

interface

type
  // Adpoted copy of TInterfacedObject for debugging
  TMyInterfacedObject = class(TObject, IInterface)
  protected
    FRefCount: Integer;
    function QueryInterface(const IID: TGUID; out Obj): HResult; stdcall;
    function _AddRef: Integer; stdcall;
    function _Release: Integer; stdcall;
  public
    procedure AfterConstruction; override;
    procedure BeforeDestruction; override;
    class function NewInstance: TObject; override;
    property RefCount: Integer read FRefCount;
  end;

implementation

uses
  Windows;

procedure TMyInterfacedObject.AfterConstruction;
begin
  InterlockedDecrement(FRefCount);
  Writeln('     AfterConstruction ', FRefCount);
end;

procedure TMyInterfacedObject.BeforeDestruction;
begin
  Writeln('     BeforeDestruction ', FRefCount);
  if RefCount <> 0 then
    System.Error(reInvalidPtr);
end;

class function TMyInterfacedObject.NewInstance: TObject;
begin
  Result := inherited NewInstance;
  TMyInterfacedObject(Result).FRefCount := 1;
  Writeln('     NewInstance ', TMyInterfacedObject(Result).FRefCount);
end;

function TMyInterfacedObject.QueryInterface(const IID: TGUID; out Obj): HResult;
begin
  Writeln('     QueryInterface ', FRefCount);
  if GetInterface(IID, Obj) then
    Result := 0
  else
    Result := E_NOINTERFACE;
end;

function TMyInterfacedObject._AddRef: Integer;
begin
  Result := InterlockedIncrement(FRefCount);
  Writeln('     _AddRef ', FRefCount);
end;

function TMyInterfacedObject._Release: Integer;
begin
  Result := InterlockedDecrement(FRefCount);
  Writeln('     _Release ', FRefCount);
  if Result = 0 then
  begin
    Writeln('     _Release Destroy');
    Destroy;
  end;
end;

end.
Run Code Online (Sandbox Code Playgroud)

--jeroen

Bar*_*lly 20

这是一个错误.RunLeakCrash中从实例到接口引用的转换应该是一个临时变量,在RunLeakCrash的持续时间内保持活动状态.

  • 我已经解决这个问题 10 年了。我不敢相信这个问题还不为人所知,所以假设它是设计的/不会解决。今天考虑它似乎很明显它可以被修复,因为它不会发生在其他托管类型(字符串、动态数组、变体等)中。 (2认同)
  • @Jeroen @Barry对QC的快速搜索表明,正如我所怀疑的,这个问题广为人知.我发现以下门票都涉及这个问题:#31164,#71015,#75036,#90025.我相当确定还有更多.#31164被解析为"设计"与评论,作者:Pierre le Riche"编译器并不总是能够保护程序员免受他/她自己的影响.当将对象和接口引用混合到同一个对象时,你必须运用必须小心避免这些问题." (2认同)

小智 5

传递对象实例创建的结果(如LeakCrash(TMyInterfacedObject.Create());)不会生成任何引用计数代码

以上是编译器错误.它必须创建一个隐藏的var并在程序存在时递减计数器