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