18. Refcount: Reference counting¶
Memory management is often a difficulty in defining an API. Should we let the user be responsible for freeing the types when they are no longer needed, or can we do it automatically on his behalf ?
The latter approach is somewhat more costly in terms of efficiency (since we need extra house keeping to know when the type is no longer needed), but provides an easier to use API.
Typically, such an approach is implemented using reference counting: all references to an object increment a counter. When a reference disappears, the counter is decremented, and when it finally reaches 0, the object is destroyed.
This approach is made convenient in Ada using controlled types. However, there are a number of issues to take care of to get things exactly right. In particular, the Ada Reference Manual specifies that Finalize should be idempotent: it could be called several times for a given object, in particular when exceptions occur.
An additional difficulty is task-safety: incrementing and decrementing the counter should be task safe, since the controlled object might be referenced from several task (the fact that other methods on the object are task safe or not is given by the user application, and cannot be ensures through the reference counting mecanism).
To make things easier, GNATColl provides the package GNATCOLL.Refcount. This package contains a generic child package.
To use it, you need to create a new tagged type that extends GNATCOLL.Refcount.Refcounted, so that it has a counter. Here is an example:
with GNATCOLL.Refcount; use GNATCOLL.Refcount;
package My_Pkg is
type My_Type is new Refcounted with record
Field1 : ...; -- Anything
end record;
package My_Type_Ptr is new Smart_Pointers (My_Type);
end My_Pkg;
The code above makes a Ref available. This is similar in semantics to an access type, although it really is a controlled type. Every time you assign the Ref, the counter is incremented. When the Ref goes out of scope, the counter is decremented, and the object is potentially freed.
Here an example of use of the package:
declare
R : Ref;
Tmp : My_Type := ...;
begin
Set (R, Tmp); -- Increment counter
Get (R).Field1 := ...; -- Access referenced object
end;
-- R out of scope, so decrement counter, and free Tmp
Although reference counting solves most of the issues with memory management, it can get tricky: when there is a cycle between two reference counted objects (one includes a reference to the other, and the other a reference to the first), their counter can never become 0, and thus they are never freed.
There are, however, common design patterns where this can severly interfer: imagine you want to have a Map, associating a name with a reference counted object. Typically, the map would be a cache of some sort. While the object exists, it should be referenced in the map. So we would like the Map to store a reference to the object. But that means the object will then never be freed while the map exists either, and memory usage will only increase.
The solution to this issue is to use weak references. These hold a pointer to an object, but do not increase its counter. As a result, the object can eventually be freed. At that point, the internal data in the weak reference is reset to null, although the weak reference object itself is still valid.
Here is an example:
with GNATCOLL.Refcount.Weakref;
use GNATCOLL.Refcount.Weakref;
type My_Type is new Weak_Refcounted with...;
package Pointers is new Weakref_Pointers (My_Type);
The above code can be used instead of the code in the first example, and provides the same capability (smart pointers, reference counted types,…). However, the type My_Type is slightly bigger, but can be used to create weak references:
WR : Weak_Ref;
declare
R : Ref;
Tmp : My_Type := ...;
begin
Set (R, Tmp); -- Increment counter
WR := Get_Weak_Ref (R); -- Get a weak reference
Get (R).Field1 := ...; -- Access referenced object
Get (Get (WR)).Field1 := ...; -- Access through weak ref
end;
-- R out of scope, so decrement counter, and free Tmp
if Get (WR) /= Null_Ref then -- access to WR still valid
-- Always true, since Tmp was freed
end if;
The example above is very simplified. Imagine, however, that you store WR in a map. Even when R is deallocated, the contents of the map remains accessible without a Storage_Error (although using Get will return Null_Ref, as above).
For task-safety issues, Get on a weak-reference returns a smart pointer. Therefore, this ensures that the object is never freed while that smart pointer object lives. As a result, we recommend the following construct in your code:
declare
R : constant Ref := Get (WR);
begin
if R /= Null_Ref then
-- Get (R) never becomes null while in this block
end if;
end;