¿Debería indicar o advertir el compilador al pasar una nueva instancia de un objeto a un método que tenga un parámetro de interfaz const de una interfaz que implemente la clase del objeto?¿Debería indicar o advertir el compilador al pasar instancias de objeto directamente como parámetros de interfaz const?
Editar: La muestra por supuesto es simple de ilustrar el problema. Pero en la vida real se vuelve mucho más complejo: ¿y si la creación y el uso están en un código que está muy alejado (unidades diferentes, clases diferentes, proyectos diferentes)? ¿Qué pasa si es mantenido por diferentes personas? ¿Qué pasa si un parámetro no const se convierte en uno const y no se puede verificar todo el código de llamada (porque la persona que cambia el código no tiene acceso a todos los códigos de llamada)?
Código como se bloquea a continuación, y es muy difícil encontrar la causa.
En primer lugar el registro:
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
continuación, el código que libera prematuramente la instancia del objeto implementar una interfaz:
//{$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.
El EInvalidPointer se manifestará dentro de la segunda llamada a Dump(Reference);
. La razón es que el recuento de referencias del objeto subyacente que expone la referencia ya es cero, por lo que el objeto subyacente ya se destruyó.
Algunas notas sobre el recuento de referencias código insertado o se omite por el compilador:
- parámetros que no aparecen con
const
(como enprocedure Dump(Reference: IInterface);
) obtener implícitas try/finally bloques para realizar el recuento de referencias. - los parámetros marcados con
const
(como enprocedure LeakCrash(const Reference: IInterface);
) no reciben ningún código de cuenta de referencias - pasando el resultado de una creación de la instancia de objeto (como
LeakCrash(TMyInterfacedObject.Create());
) no genera ningún código de cuenta de referencias
solo todos los comportamientos del compilador anteriores son muy lógicos, pero combinados pueden causar un EInvalidPointer.
El EInvalidPointer se manifiesta solo en un patrón de uso muy estrecho.
El patrón es fácil de reconocer por el compilador, pero es muy difícil depurarlo o encontrar la causa cuando lo atrapó.
La solución es bastante simple: almacenar en caché el resultado de TMyInterfacedObject.Create()
en una variable intermedia, luego pasarlo a LeakCrash()
.
¿El compilador debería advertirle o advertirle acerca de este patrón de uso?
Finalmente el código que utiliza para rastrear toda la _AddRef/_Release/etcétera llama:
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
OK; QC esto. –
He estado trabajando en esto durante 10 años. No puedo creer que el problema ya no se conociera y asumí que fue por diseño/no lo arreglaré. Hoy, al pensar en ello, parece obvio que podría solucionarse porque no ocurre con otros tipos de gestión (cadenas, matrices dyn, variantes, etc.) –
@Barry: http://qc.embarcadero.com/wc/qcmain. aspx? d = 90482 –