Должен ли компилятор предупреждать / предупреждать при передаче экземпляров объекта непосредственно в качестве параметров интерфейса const? - PullRequest
17 голосов
/ 22 декабря 2010

Должен ли компилятор намекать / предупреждать при передаче нового экземпляра объекта в метод, имеющий параметр интерфейса 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);. Причина в том, что счетчик ссылок на базовый объект, предоставляющий ссылку, уже равен нулю, поэтому базовый объект уже уничтожен.

Несколько замечаний по поводу кода подсчета ссылок, вставленного или пропущенного компилятором:

  • параметры, не отмеченные 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.

- Йерун

Ответы [ 3 ]

20 голосов
/ 22 декабря 2010

Это ошибка.Преобразование из экземпляра в ссылку на интерфейс в RunLeakCrash должно осуществляться во временную переменную, сохраняя ее работоспособной в течение всего времени RunLeakCrash.

5 голосов
/ 22 декабря 2010

передача результата создания экземпляра объекта (например, LeakCrash (TMyInterfacedObject.Create ());) не генерирует никакого кода подсчета ссылок

Выше приведена ошибка компилятора.Он должен создать скрытую переменную и уменьшить счетчик, когда существует процедура

3 голосов
/ 22 декабря 2010

Я бы проголосовал за предупреждение, поскольку даже опытные разработчики могут попасть в эту ловушку.Если кому-то не нужно это предупреждение, его можно легко отключить, что означает отсутствие изменений в отношении текущего поведения.Это похоже на предупреждения неинициализированным переменным или около того.

Другим решением этой проблемы может быть неявное Assert(InterfaceParameter.RefCount > 0); для параметров интерфейса const.Вероятно, выдается только при включенных утверждениях.

...