[fpc-devel] Suggestion: reference counted objects

Hans-Peter Diettrich DrDiettrich1 at aol.com
Sun Sep 21 21:09:24 CEST 2014


Sven Barth schrieb:
> On 20.09.2014 13:42, Sven Barth wrote:
>> On 20.09.2014 13:11, Peter Popov wrote:

> - to remedy this TObject is extended with non-virtual methods that allow 
> manual reference counting and would rely on the RTTI data I mentioned 
> (let's call the methods AddRef, Release, IsReferenceCounted and RefCount 
> for now, which can also be used to hook up the reference counting of 
> IUnknown interfaces);


I'd add a _RefCount field to TObject, regardless of whether it's really 
used later; this will fix the field offset - just like the VMT reference 
is fixed in TObject, but not in Object types. This will eliminate 
problems with class helpers.

This approach also would allow to switch any object from managed to 
unmanaged on the fly, by setting the counter to -1, because the special 
value -1 already indicates an unmanaged/const memory object (like with 
string literals).

In my first draft I considered virtual _AddRef/_Release methods, but 
calling a virtual method is more expensive than calling or inlining a 
static method.


  the code from above would then look like this to
> make it safe:
> 
> === code begin ===
> 
> function CreateObject: TObject;
> begin
>   Result := TARCObject.Create;
>   Result.AddRef;
> end;
> 
> === code end ===

Here the compiler would always insert _AddRef, just like with 
interfaces, eventually optimized (inlined?) like:
   if Result._RefCounter <> -1 then
     Result._AddRef; //or InterlockedIncrement(Result._RefCounter);


> - TObject.Free would be extended to take reference counting into account 
> as well. If the object is reference counted (IsReferenceCounted returns 
> true) it will call Release and otherwise it will continue to Destroy.
> - there would be a TARCObject declared in System which is a direct 
> descendant of TObject, but with reference counting enabled; same maybe 
> also for TInterfacedObject

The convention, of -1 meaning unmanaged, favors managed objects by 
default, when InitInstance zeroes all fields of the instance just 
created. But when the VMT reference must be excluded or inserted 
afterwards afterwards, then _RefCount can be initialized at the same 
time (to -1 for the unmanaged default). Later on a TARCObject base class 
constructor/initializer will reset _RefCount to zero again.

> - all classes can now have operator overloads as well though it should 
> be warned in the documentation that non-reference counted objects might 
> result in memory leaks there

...unless operators also test _RefCount

> - this now only leaves the problems of cycles; take this code:
> 
> === code begin ===
> 
> type
>   TSomeClass = class(TARCObject)
>     Children: specialize TList<TSomeClass>;
>     Owner: TSomeClass;
>     constructor Create(aOwner: TSomeClass);
>   end;
> 
> constructor TSomeClass.Create(aOwner: TSomeClass);
> begin
>   Children := specialize TList<TSomeClass>.Create;
>   Owner := aOwner;
>   if Assigned(Owner) then
>     Owner.Children.Add(Self);
> end;

Here I'd prefer
   Owner.AddChild(Self);
so that the Owner can implement any decent/appropriate child management 
under the hood.

> procedure Test;
> var
>   t1, t2: TSomeClass;
> begin
>   t1 := TSomeClass.Create(Nil);
>   t2 := TSomeClass.Create(t1);
>   // do something
> end;
> 
> === code end ===
> 
> Now once Test is left it would leave the instances which were assigned 
> to t1 and t2 hanging, because they have references to each other.

This depends on the implementation of TOwner.Children[] and 
TChild.Owner. Is a stored TChild.Owner reference really required in a 
managaged environment? IMO a (strong) unidirectional reference from 
Owner to Child will do it all. Then no child will be destroyed, as long 
as its owner holds a reference to it. That's the intended purpose of 
both owner/child and automatic memory management.

When it's desireable to definitely destroy an owned object at will, then 
its owner must be known, of course. In this case two different 
management approaches conflict with each other. In this case I'd accept 
a weak Owner reference, because the referenced Owner will stay alive 
longer than it's listed children.

More problematic are circular references without a decicated owner/child 
relationship.


> There are (as far as I see) three ways to solve this:
> * provide a way to break the circle (in this example e.g. setting Owner 
> to Nil before leaving Test; this is what Delphi provides with the 
> DisposeOf virtual method)
> * introduce weak references which would disable reference counting, e.g.:
> 
> === code begin ===
> 
> type
>   TSomeClass = class(TARCObject)
>     // ...
>     Owner: TSomeClass weak;
>     // ...
>   end;
> 
> === code end ===
> 
> Now the "TSomeClass.Create(t1)" line in "Test" wouldn't increase the 
> reference count of "t1" further and thus both class instances would be 
> destroyed after "Test" is left.

This IMO is the preferable way to go, in a definite owner/child 
relationship. The lifetime of an owner can not depend on the existence 
of owned children, so that the owner will survive until it has 
destroyed/released all his children himself. A child-to-owner reference 
is not required in automatic management, it only is required when it 
must be possible to definitely destroy an owned child object.


Otherwise cyclic references *without* a definite owner/child 
relationship are really problematic:

> * provide a possibilty to execute a cycle detection algorithm during the 
> Release part of the reference counting; this has the benefit of avoiding 
> the need for "weak", but there would be the problem that the algorithm 
> can be potentially expensive especially with large object instance 
> hierarchies (think LCL here) and this would also need to be executed for 
> *each* decrement of the reference count, thus for both the automated one 
> (which till now could have been rather efficient) and the manual one.

That's the classic mark/sweep approach, with all its known drawbacks; in 
detail its asynchronous destruction of objects is a known troublemaker 
in OOP, see "Why a garbage collector never should call an destructor...".

In such cases IMO the developer should establish a definite owner/child 
relationship, by e.g. making all but one reference Weak, or by a special 
(intelligent) _Release method.


Even if I mentioned the use of Weak references as possible solutions of 
some problems, I still don't favor such solutions for the already 
mentioned reasons.

DoDi




More information about the fpc-devel mailing list