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

18

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

编辑示例当然很简单,只是为了说明问题。但在现实生活中,它变得更加复杂:如果创建和使用的代码相距甚远(不同的单元、不同的类、不同的项目)怎么办?如果由不同的人维护呢?如果非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

接下来是过早释放实现接口的对象实例的代码:

//{$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.

当第二次调用Dump(Reference);时,EInvalidPointer错误会出现。原因是表露Reference的基础对象的引用计数已经为零,所以基础对象已经被销毁。

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

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

所有上述编译器行为都非常合乎逻辑,但结合在一起可能会导致EInvalidPointer。
EInvalidPointer只在非常狭窄的使用模式下显现。
该模式很容易由编译器识别,但当你遇到它时,很难进行调试或找到原因。
解决方法非常简单:将TMyInterfacedObject.Create()的结果缓存到一个中间变量中,然后将其传递给LeakCrash()

编译器是否应该提示或警告您有关此使用模式的信息?

最后,以下是我用于跟踪所有_AddRef/_Release/等调用的代码:

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.

--jeroen

3个回答

21

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


2
我已经围绕这个问题工作了10年。我无法相信这个问题早就不为人所知,因此被认为是设计上的问题/不会修复。今天思考它时,很明显它可以修复,因为它不会发生在其他受管理类型(字符串、动态数组、变量等)中。 - David Heffernan
3
在 QC 上进行快速搜索显示,正如我所猜测的那样,这个问题被广泛知晓。我找到了以下参考此问题的工单:#31164、#71015、#75036、#90025。我相当确定还有更多。#31164 被解决为“按设计”,并由 Pierre le Riche 发表了以下评论:“编译器不能总是保护程序员免受自己的错误影响。当混合对象和接口引用同一对象时,您必须采取必要的注意措施以避免出现此类问题。” - David Heffernan
@Jeroen @Barry 显然,在QC上报告和投票这个问题是行不通的。如果Barry能够从内部做些什么,那就太好了——我会非常感激! - David Heffernan

5

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

上述是编译器错误。它必须创建一个隐藏的变量,并在过程退出时减少计数器。


3

我会建议加上一个警告,因为即使有经验的开发者也可能会陷入这个陷阱。如果有人不想要这个警告,可以轻松地禁用它,这意味着与当前行为相比没有任何变化。这有点像对未初始化变量的警告等。

解决这个问题的另一个方法可能是对于常量接口参数隐式地添加 Assert(InterfaceParameter.RefCount > 0);。可能只在启用断言时发出。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接