将对象实例直接作为 const 接口参数传递时,编译器是否应该提示/警告? [英] Should the compiler hint/warn when passing object instances directly as const interface parameters?

查看:19
本文介绍了将对象实例直接作为 const 接口参数传递时,编译器是否应该提示/警告?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

Should the compiler hint/warn when passing a new instance of an object to a method having a const interface parameter of an interface that the object's class implements?

编辑:示例当然很容易说明问题.但在现实生活中,它变得更加复杂:如果创建和使用的代码相距甚远(不同的单元、不同的类、不同的项目)怎么办?如果由不同的人维护呢?如果非常量参数变成了常量参数,并且不是所有调用代码都可以检查(因为更改代码的人无法访问所有调用代码)怎么办?

像下面这样的代码崩溃,很难找到原因.

Code like below crashes, and it is very hard to find the cause.

首先是日志:

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

那么过早释放实现接口的对象实例的代码:

Then the code that prematurely releases the object instance implementing an interface:

//{$define all}

program InterfaceConstParmetersAndPrematureFreeingProject;

{$APPTYPE CONSOLE}

uses
  SysUtils,
  Windows,
  MyInterfacedObjectUnit in '..srcMyInterfacedObjectUnit.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的底层对象的引用计数已经为零,所以底层对象已经被销毁了.

The EInvalidPointer will manifest itself inside the second call to Dump(Reference);. The reason is that the reference count of the underlying object exposing Reference is already zero, so the underlying object got already destroyed.

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

A few notes on reference counting code inserted or omitted by the compiler:

  • 未用 const 标记的参数(如在 procedure Dump(Reference: IInterface); 中)获得隐式 try/finally 块以执行引用计数.
  • const标记的参数(如procedure LeakCrash(const Reference: IInterface);)不会得到任何引用计数代码
  • 传递对象实例创建的结果(如LeakCrash(TMyInterfacedObject.Create());)不会生成任何引用计数代码
  • parameters not marked with const (like in procedure Dump(Reference: IInterface);) get implicit try/finally blocks to perform reference counting.
  • parameters marked with const (like in procedure LeakCrash(const Reference: IInterface);) do not get any reference counting code
  • passing the result of an object instance creation (like LeakCrash(TMyInterfacedObject.Create());) does not generate any reference counting code

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

Alone all of the above compiler behaviours are very logical, but combined they can cause an EInvalidPointer.
The EInvalidPointer manifests itself only in a very narrow usage pattern.
The pattern is easy to recognize by the compiler, but very hard to debug or find the cause when you trapped into it.
The workaround is pretty simple: cache the result of TMyInterfacedObject.Create() in an intermediate variable, then pass it on to LeakCrash().

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

Should the compiler hint or warn you about this usage pattern?

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

Finally the code I used to trace all the _AddRef/_Release/etcetera calls:

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

推荐答案

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

It's a bug. The conversion from instance to interface reference in RunLeakCrash should be to a temporary variable, keeping it alive for the duration of RunLeakCrash.

这篇关于将对象实例直接作为 const 接口参数传递时,编译器是否应该提示/警告?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆