2010-12-22 7 views
16

¿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 en procedure Dump(Reference: IInterface);) obtener implícitas try/finally bloques para realizar el recuento de referencias.
  • los parámetros marcados con const (como en procedure 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

Respuesta

20

Es un error. La conversión de la instancia a la referencia de la interfaz en RunLeakCrash debe ser una variable temporal, manteniéndola activa mientras dure RunLeakCrash.

+0

OK; QC esto. –

+1

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.) –

+0

@Barry: http://qc.embarcadero.com/wc/qcmain. aspx? d = 90482 –

3

votaría por una advertencia, ya que incluso los desarrolladores experimentados podrían caer en esta trampa. Si alguien no quiere esa advertencia, se puede desactivar fácilmente, lo que significa que no hay cambios con respecto al comportamiento actual. Es algo así como advertencias a variables no inicializadas o algo así.

Otra solución a este problema podría ser implícita Assert(InterfaceParameter.RefCount > 0); para parámetros de interfaz const. Probablemente solo se emita cuando las aserciones están activadas.

5

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

Arriba está el error del compilador. Debe crear una var oculta y disminuir el contador cuando existe el procedimiento

Cuestiones relacionadas