openlang

Interface references and object references to the same instance

The lifetime mistake I meet most often in older Delphi code comes from mixing two ownership rules on one instance. A class that descends from TInterfacedObject is freed when its interface reference count reaches zero. A variable of the class type is not counted at all. Hold the same instance through both kinds of reference and the two rules disagree about when it dies.

program Greeter;
{$APPTYPE CONSOLE}
type
  IGreeter = interface
    procedure Hello;
  end;

  TGreeter = class(TInterfacedObject, IGreeter)
    procedure Hello;
  end;

procedure TGreeter.Hello;
begin
  WriteLn('hello');
end;

procedure Use(G: IGreeter);
begin
  G.Hello;
end;

var
  Obj: TGreeter;
begin
  Obj := TGreeter.Create; // count 0, a class reference is not counted
  Use(Obj);               // 1 inside Use, back to 0 on the way out
  Obj.Free;               // the object is already gone
end.

The instance starts with a count of zero, because the class reference never touched it. Passing it to Use converts it to IGreeter, the count goes to one for the duration of the call and back to zero on the way out, and at zero the object destroys itself. The Obj.Free that follows is a second destruction of memory that has already been released. The compiler accepts all of it.

My rule is that once a class implements interfaces through TInterfacedObject, every long-lived reference to an instance is an interface reference, and the class type appears only in the line that calls the constructor. I am curious whether others draw the line in the same place.

后续讨论 5

顺着彼此的依据读下去
  1. 固定链接

    Let me make sure I have the order right, because the timing is the whole question. One, Create returns with the count at zero. Two, entering Use takes it to one. Three, leaving Use drops it to zero, and the destructor runs right there, in the finalization code of Use, before control is back in the caller. So by the time Obj.Free runs, the destruction has already happened, not merely been scheduled.

    If that is right, does declaring the parameter as const G: IGreeter remove steps two and three entirely? I have read that it does, and that it opens a different problem when the argument is a freshly constructed object.

  2. 固定链接

    The line I'd look at is the declaration, Obj: TGreeter. Change it to Obj: IGreeter and the example stops compiling at Obj.Free, because Free belongs to TObject and the interface doesn't declare it. That's a nice property of your rule. The compiler then refuses the second destruction instead of letting it through.

    It only holds while the interface really is narrower than the class, though. If someone adds a Dispose method to IGreeter for convenience, the variable's type stops telling you anything about who frees what.

  3. 固定链接

    Agree on the declaration, and there's one more uncounted reference the variable's type won't show you. Assign Obj.Hello to a procedure of object field somewhere and that method pointer holds the instance too, with no count. The interface references drop to zero, the object goes, and the event still fires into it later. In the editor everything looks like interface code.

    C# has a cousin of this with event handlers keeping subscribers alive, the opposite direction, which is why I went looking for it here.

  4. 固定链接

    The rule also gives a cheap check. If the class type only appears next to Create, a text search for : TGreeter should come back empty, and whatever it does return is the list to go through.

  5. 固定链接

    Your three steps are right, and the destruction does happen inside Use, as the last thing it does before returning. With const G: IGreeter the callee does no counting at all, so steps two and three disappear and the example above stops destroying the object early.

    The other problem you read about concerns Use(TGreeter.Create). With a const parameter nothing in that call owns the new instance, because there is no variable on the caller's side to hold a count. I have seen it described as a leak, and I am not certain every compiler version treats it the same way, so I simply do not write that form. The instance goes into an interface variable first and is passed from there.

    I should add one exception to my own rule. TComponent implements _AddRef and _Release without counting, so a component reached through an interface is still owned by its Owner. The rule applies to TInterfacedObject descendants and not to every class that implements an interface.

同一个问题的其他角度

  1. Assigning realloc back to the only pointer leaks on failure

    *buffer = realloc(*buffer, n) turns up in a lot of growth helpers, and it is wrong in a specific way. If the allocation fails…

    回复 8最近活动 clua
  2. What survives a catalog reload?

    A slice is an address and a length. Neither field keeps the backing allocation alive, and the Zig reference is plain that poi…

    回复 15最近活动 zigswiftrustcppodin
  3. The dangling pointer warning went away, the pointer did not

    Passing a name into a C++ library through a wrapper, and the first version was one line, CString::new(name).unwrap().as_ptr()…

    回复 3最近活动 rustcpp