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

Posted

技术标签:

【中文标题】将对象实例直接作为 const 接口参数传递时,编译器是不是应该提示/警告?【英文标题】:Should the compiler hint/warn when passing object instances directly as const interface parameters?将对象实例直接作为 const 接口参数传递时,编译器是否应该提示/警告? 【发布时间】:2011-05-29 09:13:04 【问题描述】:

在将对象的新实例传递给具有对象类实现的接口的 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.

EInvalidPointer 将在对Dump(Reference); 的第二次调用中显现。 原因是暴露Reference的底层对象的引用计数已经为零,所以底层对象已经被销毁了。

关于编译器插入或省略的引用计数代码的几点说明:

未用const 标记的参数(如procedure Dump(Reference: IInterface); 中的参数)获取隐式try/finally 块来执行引用计数。 标有const 的参数(如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.

--杰罗恩

【问题讨论】:

【参考方案1】:

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

【讨论】:

我已经为此工作了 10 年。我不敢相信这个问题还不为人所知,因此假设它是设计使然/无法解决。今天想想,它似乎很明显可以修复,因为它不会发生在其他托管类型(字符串、dyn 数组、变体等)中。 @Jeroen @Barry 对 QC 的快速搜索表明,正如我所怀疑的那样,这个问题是众所周知的。我发现以下票证都提到了这个问题:#31164、#71015、#75036、#90025。我相当肯定还有更多。 #31164 被 Pierre le Riche 用评论解析为“按设计”“编译器并不总是可以保护程序员免受他/她自己的伤害。当混合对象和接口引用到同一个对象时,你必须练习必要的注意以避免此类问题。” @Jeroen @Barry 很明显,在 QC 上对此问题进行报告和投票是无法完成的。如果巴里能从内部做点什么,那就太好了——我将非常感激!【参考方案2】:

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

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

【讨论】:

【参考方案3】:

我会投票赞成警告,因为即使是经验丰富的开发人员也可能落入这个陷阱。如果有人不想要那个警告,它可以很容易地被禁用,这意味着当前行为没有改变。这有点像对未初始化变量的警告。

这个问题的另一个解决方案可能是隐式Assert(InterfaceParameter.RefCount &gt; 0); 用于 const 接口参数。可能仅在断言打开时发出。

【讨论】:

以上是关于将对象实例直接作为 const 接口参数传递时,编译器是不是应该提示/警告?的主要内容,如果未能解决你的问题,请参考以下文章

MVC 3 将实体作为接口传递

将数组作为 C++ 中方法的 const 参数传递

GroovyGroovy 方法调用 ( Java 中函数参数是接口类型 | 函数参数是接口类型 可以 直接传递闭包 )

C++ Boost - 序列化错误 - 将“const B”作为“this”参数传递会丢弃限定符

将类对象实例作为参数从 main() 传递给另一个类对象实例

首先通过将类型作为参数传递来动态实例化实体框架数据库中的模型对象