Self destructing object instance
Using reference counting classes always requires some caution. You have to pay attention to reference cycles, using interface reference for referencing such object instances, not calling FreeAndNil and more... it is quite a list...
But every once in a while, new ways of shooting yourself in the foot keep popping up... So here goes the story...
I have two reference counted classes that hold reference to each other instances. One of those references is marked as
Why is destructor called during construction of an object here and why there is no exception?
Reference counting basically counts strong references to an object instance and when last strong reference to an object goes out of scope, reference count will drop to 0 and instance will be destroyed.
Strong references here represent interface references (object references and pointers don't trigger reference counting mechanism) and compiler inserts calls to
Simplified those methods generally look like:
Construction of reference counted object instance looks like:
Why is initial reference count in
Initial reference count must be set to 1 because code in constructors can be complex and can trigger transient reference counting which could automatically destroy the object during the construction process before it has chance to be assigned to the initial strong reference that will keep it alive.
That initial reference count is then decreased in
While object instance is protected from self destruction inside constructor chain, for a brief moment it will be in fragile state inside
Actual code that triggers reference counting in this case is hidden in rather unexpected place and it comes in form of
But every once in a while, new ways of shooting yourself in the foot keep popping up... So here goes the story...
I have two reference counted classes that hold reference to each other instances. One of those references is marked as
[weak]
to prevent creating strong reference cycle.
type
TFoo = class(TInterfacedObject)
private
[weak]
FRef: IInterface;
public
constructor Create(const ARef: IInterface);
end;
TBar = class(TInterfacedObject)
private
FFoo: IInterface;
public
constructor Create; virtual;
destructor Destroy; override;
procedure AfterConstruction; override;
end;
constructor TFoo.Create(const ARef: IInterface);
begin
inherited Create;
FRef := ARef;
end;
constructor TBar.Create;
begin
inherited;
end;
destructor TBar.Destroy;
begin
inherited;
end;
procedure TBar.AfterConstruction;
begin
inherited;
FFoo := TFoo.Create(Self);
end;
procedure Test;
var
Intf: IInterface;
begin
Intf := TBar.Create;
writeln(Assigned(Intf)); // TRUE as expected
end; // AV here
But I cannot successfully finish construction of TBar
object instance and exiting Test procedure triggers Access Violation exception at _IntfClear
.Exception class $C0000005 with message 'access violation at 0x0040e398: read of address 0x00000009'.Stepping through debugger shows that
TBar.Destroy
is called before code reaches writeln(Assigned(Intf))
line and there is no exception during construction process.Why is destructor called during construction of an object here and why there is no exception?
Reference counting overview
To understand what is happening here we need short overview of how Delphi ARC works on reference counted object instances (ones implementing some interface) under classic compiler.Reference counting basically counts strong references to an object instance and when last strong reference to an object goes out of scope, reference count will drop to 0 and instance will be destroyed.
Strong references here represent interface references (object references and pointers don't trigger reference counting mechanism) and compiler inserts calls to
_AddRef
and _Release
methods at appropriate places for incrementing and decrementing reference count. For instance, when assigning to interface _AddRef
is called, and when that reference goes out of scope _Release
.Simplified those methods generally look like:
function TInterfacedObject._AddRef: Integer;
begin
Result := AtomicIncrement(FRefCount);
end;
function TInterfacedObject._Release: Integer;
begin
Result := AtomicDecrement(FRefCount);
if Result = 0 then
Destroy;
end;
Construction of reference counted object instance looks like:
- construction -
TInterfacedObject.Create -> RefCount = 0
- executing
NewInstance
- executing chain of constructors
- executing
AfterConstruction
chain
- assigning to initial strong reference
Intf := ...
_AddRef -> RefCount = 1
NewInstance
and AfterConstruction
methods
class function TInterfacedObject.NewInstance: TObject;
begin
Result := inherited NewInstance;
TInterfacedObject(Result).FRefCount := 1;
end;
procedure TInterfacedObject.AfterConstruction;
begin
AtomicDecrement(FRefCount);
end;
Why is initial reference count in NewInstance
set to 1 and not to 0?
Initial reference count must be set to 1 because code in constructors can be complex and can trigger transient reference counting which could automatically destroy the object during the construction process before it has chance to be assigned to the initial strong reference that will keep it alive.That initial reference count is then decreased in
AfterConstruction
and object instance reference count is properly set for further reference counting.Problem
Real problem in this questions code is in fact that it triggers transient reference counting inAfterConstruction
method after call to inherited
which decreases initial object reference count back to 0. Because of that, object will have its count increased, then decreased to 0 and it will self destruct calling Destroy
.While object instance is protected from self destruction inside constructor chain, for a brief moment it will be in fragile state inside
AfterConstruction
method and we need to make sure that there is no code there that can trigger reference counting mechanism during that time.Actual code that triggers reference counting in this case is hidden in rather unexpected place and it comes in form of
[weak]
attribute. So, the very thing that should prevent instance from participating in reference counting mechanism actually triggers it - this is a flaw in [weak]
attribute design reported as RSP-20406.Solution(s)
- If possible, move code that can trigger reference counting from
AfterConstruction
to constructor - Call
inherited
at the end ofAfterConstruction
method instead of the beginning. - Perform some explicit reference counting on your own by calling
AtomicIncrement(FRefCount)
at the beginning andAtomicDecrement(FRefCount)
at the end ofAfterConstruction
(you cannot use_Release
because it will destroy the object) - Replace
[weak]
attribute with[unsafe]
(this can only be done ifTFoo
instance lifetime will never exceedTBar
instance lifetime
Comments
Post a Comment