Rationale for Ada 2012

John Barnes
Contents   Index   References   Search   Previous   Next 

6.4 Access types and storage pools

A significant change in Ada 2005 was the introduction of anonymous access types. It is believed that the motivation was to remove the feeling that Ada 95 was unnecessarily pedantic in requiring the introduction of lots of named access types whereas in languages such as C one can just place a star on the identifier of the type being referenced in order to introduce a pointer type.
However, anonymous access types raised more complex accessibility check problems which did not arise with named access types. Most of these problems were resolved in the definition of Ada 2005 but one remained concerning stand-alone objects of anonymous access types. Interestingly, such stand-alone objects were added to Ada 2005 late in the development process; perhaps hastily as it turned out.
In Ada 2005, local stand-alone objects take the accessibility level of the master in which they are declared.
Consider an attempt to use a local stand-alone object in an algorithm to reverse a list. We assume that the list comprises nodes of the following type
type Node is
      Next: access Node;
   end record;
and we write
function Reverse_List(List: access Node) return access Node is
   Result: access Node := null;
   This_Node: access Node := List;
   Next_Node: access Node := null;
   while This_Node /= null loop
      Next_Node := This_Node.Next;
      This_Node.Next := Result;    -- access failure in 2005
      Result := This_Node;
      This_Node := Next_Node;
   end loop;
   return Result;    -- access failure in 2005
end Reverse_List;
This uses the obvious algorithm of working down the list and rebuilding it. However, in Ada 2005 there are two accessibility failures associated with the variable Result. The assignment to This_Node.Next fails because Result might be referring to something local and we cannot assign that to a node of the list since the list itself lies outside the scope of Reverse_List. Similarly, attempting to return the value in Result fails.
The problem with returning a result can sometimes be solved by using an extended return statement as illustrated in [2]. But this is not a general remedy. The problem is solved in Ada 2012 by treating stand-alone access objects rather like access parameters so that they carry the accessibility of the last value assigned to them as part of their value.
Another reason for introducing anonymous access types in Ada 2005 was to reduce the need for explicit type conversions (note that anonymous access types naturally have no name to use in an explicit conversion). However, it turns out that in practice it is convenient to use anonymous access types in some contexts (such as the component Next of type Node) but in other contexts we might find it logical to use a named access type such as
type List is access Node;
In Ada 2005, explicit conversions are often required from anonymous access types to named general access types and this has been considered to be irritating. Accordingly, the rule has been changed in Ada 2012 to say that an explicit conversion is only required if the conversion could fail.
This relaxation covers both accessibility checks and tag checks. For example we might have
type Class_Acc is access all T'Class;    -- named general access type
type Rec is
      Comp: access T'Class;    -- anon type
   end record;
R: Rec;
and then some code somewhere
Z: Class_Acc;
Z := R.Comp;    -- OK in Ada 2012
The conversion from the anonymous type of Comp to the named type Class_Acc of Z on the assignment to Z cannot fail and so does not require an explicit conversion whereas it did in Ada 2005. However, a conversion from a stand-alone access object or an access parameter always requires an explicit conversion to check the accessibility level carried as part of the value as explained above since such a check could fail.
With regard to tag checks, if it is statically known that the designated type of the anonymous access type is covered by the designated type of the named access type then there is no need for a tag check and so an explicit conversion is not required.
It will be recalled that there is a fictitious type known as universal_access (much as universal_integer, root_Integer and so on). For example, the literal null is of this universal type. Moreover, there is a function "=" used to compare universal_access values. Permitting implicit conversions requires the introduction of a preference rule for the equality operator of the universal type. Suppose we have
type A is access Integer;
R, S: access Integer;
if R = S then
Now since we can do an implicit conversion from the anonymous access type of R and S to the type A, there is confusion as to whether the comparison uses the equality operator of the type universal_access or that of the type A. Accordingly, there is a preference rule that states that in the case of ambiguity there is a preference for equality of the type universal_access. Similar preference rules already apply to root_integer and root_real.
A related topic concerns membership tests which were described in Section 3.6 of the chapter on Expressions.
If we want to ensure that a conversion from perhaps Integer to Index will work and not raise Constraint_Error we can write
subtype Index is Integer range 1 .. 20;
I: Index;
K: Integer;
if K in Index then
   I := Index(K);    -- bound to work
   ...    -- remedial action
end if;
This is much neater than attempting the conversion and then handling Constraint_Error.
However, in Ada 2005, there is no similar facility for testing to see whether an access type conversion would fail. So membership tests in Ada 2012 are extended to permit such a test. So if we have
type A is access T1;
X: A;
type Rec is
      Comp: access T2;
   end record;
R: Rec;
Y: access T2;
we can write
if R.Comp in A then
   X := A(R.Comp)    -- conversion bound to work
else ...
The membership test will return true if the type T1 covers T2 and the accessibility rules are satisfied so that the conversion is bound to work. Note that the converted expression (R.Comp in this case) can be an access parameter or a stand-alone access object such as Y; in these cases a dynamic test may be required.
Another useful application of membership tests is in preconditions where we might want to ensure that an actual parameter meets some accessibility condition. Suppose we have an access type declared at library level thus
type General is access all T'Class;
and a primitive subprogram of type T
procedure Do_It(D: access T);
Moreover, perhaps the body of Do_It needs to assign D to an object of the type General. This could cause an accessibility check to fail and raise Program_Error in the body which might surprise the user. This can be guarded against by adding
with Pre => D in General;
to the specification of Do_It so that the user is told of any accessibility problem at the point of call rather than in the body.
We now turn to consider various features concerning allocation and storage pools.
It will be recalled that if we write our own storage pools then we have to declare a pool type derived from the type Root_Storage_Pool in the package System.Storage_Pools. So we might write
package My_Pools is
   type Pond(Size: Storage_Count) is new Root_Storage_Pool with private;
where the discriminant gives the size of the pool. We then have to provide procedures Allocate and Deallocate for our own pool type Pond corresponding to those for Root_Storage_Pool. The procedures Allocate and Deallocate both have four parameters. For example, the procedure Allocate is
procedure Allocate    (Pool: in out Root_Storage_Pool;
                       Storage_Address: out Address;
                       Alignment: in Storage_Count) is abstract;
When we declare our own Allocate we do not have to use the same names for the formal parameters. So we might more simply write
procedure Allocate    (Pool: in out Pond;
                       Addr: out Address;
                       SISE: in Storage_Count;
                       Align: in Storage_Count);}
As well as Allocate and Deallocate we also have to write a function Storage_Size and procedures Initialize and Finalize. However, the key procedures are Allocate and Deallocate which give the algorithms for determining how the storage in the pool is manipulated.
Two parameters of Allocate give the size and alignment of the space to be allocated. However, it is possible that the particular algorithm devised might need to know the worst case values in determining an appropriate strategy. The attribute Max_Size_In_Storage_Elements gives the worst case for the storage size in Ada 2005 but there is no corresponding attribute for the worst case alignment.
This is overcome in Ada 2012 by the provision of the attribute Max_Alignment_For_Allocation. There are various reasons for possibly requiring a different alignment to that expected. For example, the raw objects might simply be byte aligned but the algorithm might decide to append dope or monitoring information which is integer aligned.
The collector of Ada curiosities might remember that Max_Size_In_Storage_Elements is the attribute with most characters in Ada 2005 (28 of which 4 are underlines). Curiously, Max_Alignment_For_Allocation also has 28 characters of which only 3 are underlines.
There are problems with anonymous access types and allocation. Consider
package P is
   procedure Proc(X: access Integer);
end P;
with P;
procedure Try_This is
   P.Proc(new Integer'(10));
end Try_This;
The procedure Proc has an access parameter X and the call of Proc in Try_This does an allocation with the literal 10. Where does it go? Which pool? Can we do Unchecked_Deallocation? There are special rules for allocators of anonymous access types which aim to answer such questions. The pool is "created at the point of the allocator" and so on.
But various problems arise. An important one is that it is not possible to do unchecked deallocation because the access type has no name; this is particularly serious with library level anonymous access types. An example of such a type might be that of the component Next if the record type Node discussed earlier had been declared at library level.
Consequently, it was concluded that it is best to use named access types if allocation is to be performed. We can always convert to an anonymous type if desired after the allocation has been performed.
In order to avoid encountering such problems a new restriction identifier is introduced. So writing
pragma Restrictions(No_Anonymous_Allocators);
prevents allocators of anonymous access types and so makes the call of the procedure Proc in the procedure Try_This illegal.
Many long-lived control programs have a start-up phase in which various storage structures are established and which is then followed by the production phase in which various restrictions may be imposed. Ada 2012 has a number of features that enable this to be organized and monitored.
One such feature is the new restriction
pragma Restrictions(No_Standard_Allocators_After_Elaboration);}
This specifies that an allocator using a standard storage pool shall not occur within a parameterless library subprogram or within the statements of a task body. In essence this means that all such allocation must occur during library unit elaboration. Storage_Error is raised if allocation occurs afterwards.
However, it is expected that systems will permit some use of user-defined storage pools. To enable the writers of such pools to monitor their use some additional functions are added to the package Task_Identification so that it now takes the form
package Ada.Task_Identification is
   type Task_Id is private;
   function Current_Task return Task_Id;
   function Environment_Task return Task_Id;
   procedure Abort_Task(T: in Task_Id);
   function Is_Terminated(T: Task_Id) return Boolean;
   function Is_Callable(T: Task_Id) return Boolean;
   function Activation_Is_Complete(T: Task_Id) return Boolean;
end Ada.Task_Identification;
The new function Environment_Task returns the identification of the environment task. The function Activation_Is_Complete returns true if the task concerned has finished activation. Moreover, if Activation_Is_Complete is applied to the environment task then it indicates whether all library items of the partition have been elaborated.
A major new facility is the introduction of subpools. This is an extensive subject so we give only an overview. The general idea is that one wants to manage heaps with different lifetimes. It is often the case that an access type is declared at library level but various groups of objects of the type are declared and so could be reclaimed at a more nested level. This is done by splitting a pool into separately reclaimable subpools. This is far safer and often cheaper than trying to associate lifetimes with individual objects.
A new child package of System.Storage_Pools is declared thus
package System.Storage_Pools.Subpools is
   pragma Preelaborate(Subpools);
   type Root_Storage_Pool_With_Subpools is
         abstract new Root_Storage_Pool with private;
   type Root_Subpool is abstract tagged limited private;
   type Subpool_Handle is access all Root_Subpool'Class;
      for Subpool_Handle'Storage_Size use 0;
   function Create_Subpool
           (Pool: in out Root_Storage_Pool_With_Subpools)
                      return not null Subpool_Handle is abstract;
   function Pool_of_Subpool
          (Subpool: not null Subpool_Handle)
                      return access Root_Storage_Pool_With_Subpools'Class;
   procedure Set_Pool_of_Subpool
        (Subpool: not null Subpool_Handle;
         To: in out Root_Storage_Pool_With_Subpools'Class);
   procedure Allocate_From_Subpool(
         Pool: in out Root_Storage_Pool_With_Subpools;
         Storage_Address: out Address;
         Size_In_Storage_Elements: in Storage_Count;
         Alignment: in Storage_Count;
         Subpool: in not null Subpool_Handle) is abstract
         with Pre'Class => Pool_of_Subpool(Subpool) = Pool'Access;
   procedure Deallocate_Subpool(
         Pool: in out Root_Storage_Pool_With_Subpools;
         Subpool: in out Subpool_Handle) is abstract
         with Pre'Class => Pool_of_Subpool(Subpool) = Pool'Access;
   function Default_Subpool_for_Pool
         (Pool: in out Root_Storage_Pool_With_Subpools)
                      return not null Subpool_Handle;
   procedure Allocate(
         Pool: in out Root_Storage_Pool_With_Subpools;
         Storage_Address: out Address;
         Size_In_Storage_Elements: in Storage_Count;
         Alignment: in Storage_Count);
   procedure Deallocate( ... ) is null;
   function Storage_Size (Pool : Root_Storage_Pool_With_Subpools)
           return Storage_Count is
   ... -- not specified by the language
end System.Storage_Pools.Subpools;}
If we wish to declare a storage pool that can have subpools then rather than declare an object of the type Root_Storage_Pool in the package System.Storage_Pools we have to declare an object of the derived type Root_Storage_Pool_With_Subpools declared in the child package.
The type Root_Storage_Pool_With_Subpools inherits operations Allocate, Deallocate and Storage_Size from the parent type. Remember that Allocate and Deallocate are automatically called by the compiled code when items are allocated and deallocated. In the case of subpools we don't need Deallocate to do anything so it is null. The function Storage_Size determines the value of the attribute Storage_Size and is given by a function expression.
Subpools are separately reclaimable parts of a storage pool and are identified and manipulated by objects of the type Subpool_Handle (these are access values). We can create a subpool by a call of Create_Subpool. So we might have (assuming appropriate with and use clauses)
package My_Pools is
   type Pond(Size: Storage_Count) is
   new Root_Storage_Pool_With_Subpools with private;
   subtype My_Handle is Subpool_Handle;
and then
My_Pool: Pond(Size => 1000);
Puddle: My_Handle := Create_Subpool(My_Pool);
The implementation of Create_Subpool should call
Set_Pool_Of_Subpool(Puddle, My_Pool);
before returning the handle. This enables various checks to be made.
In order to allocate an object of type T from a subpool, we have to use a new form of allocator. But first we must ensure that T is associated with the pool itself. So we might write
type T_Ptr is access T;
for T_Ptr'Storage_Pool use My_Pool;
And then to allocate an object from the subpool identified by the handle Puddle we write
X := new (Puddle) T'( ... );
where the subpool handle is given in parentheses following new.
Of course we don't have to allocate all such objects from a specified subpool since we can still write
Y := new T'( ... );
and the object will be allocated from the parent pool My_Pool. It is actually allocated from a default subpool in the parent pool and this is determined by writing a suitable body for the function Default_Subpool_for_Pool and this is called automatically by the allocation mechanism. Note that in effect the whole of the pool is divided into subpools one of which may be the default subpool. If we don't provide an overriding body for Default_Subpool_for_Pool then Program_Error is raised. (Note that this function has a parameter of mode in out for reasons that need not bother us.)
The implementation carries out various checks. For example, it will check that a handle refers to a subpool of the correct pool by calling the function Pool_Of_Subpool. Both this function and Set_Pool_Of_Subpool are provided by the Ada implementation and typically do not need to be overridden by the implementer of a particular type derived from Root_Storage_Pool_With_Subpools.
In the case of allocation from a subpool, the procedure Allocate_From_Subpool rather than Allocate is automatically called. Note the precondition to check that all is well.
It will be recalled that for normal storage pools, Deallocate is automatically called from an instance of Unchecked_Deallocation. In the case of subpools the general idea is that we get rid of the whole subpool rather than individual items in it. Accordingly, Deallocate does nothing as mentioned earlier and there is no Deallocate_From_Subpool. Instead we have to write a suitable implementation of Deallocate_Subpool. Note again the precondition to check that the subpool belongs to the pool.
Deallocate_Subpool is called automatically as a consequence of calling the following library procedure
with System.Storage_Pools.Subpools;
use System.Storage_Pools.Subpools;
procedure Ada.Unchecked_Deallocate_Subpool(Subpool: in out Subpool_Handle);
So when we have finished with the subpool Puddle we can write
and the handle becomes null. Appropriate finalization also takes place.
In summary, the writer of a subpool implementation typically only has to provide Create_Subpool, Allocate_From_Subpool and Deallocate_Subpool since the other subprograms are provided by the Ada implementation of the package System.Storage_Pools.Subpools and can be inherited unchanged.
An example of an implementation will be found in subclause RM (13.11.6) of the RM. This shows an implementation of a Mark/Release pool in a package MR_Pool. Readers are invited to create variants called perhaps Miss_Pool and Dr_Pool!
Further control over the use of storage pools (nothing to do with subpools) is provided by the ability to define our own default storage pool as mentioned in the Introduction (see 1.3.5). Thus we can write (and completing our Happy Family of Pools)
pragma Default_Storage_Pool(Master_Pool);
and then all allocation within the scope of the pragma will be from Master_Pool unless a different specific pool is given for a type. This could be done by using an attribute definition clause thus
type Cell_Ptr is access Cell;
for Cell_Ptr'Storage_Pool use Cell_Ptr_Pool;
or by using an aspect specification thus
type Cell_Ptr is access Cell
   with Storage_Pool => Cell_Ptr_Pool;
A pragma Default_Storage_Pool can be overridden by another one so that for example all allocation in a package (and its children) is from another pool.
The default pool can be specified as null thus
pragma Default_Storage_Pool(null);
and this prevents any allocation from standard pools.
Allocation normally occurs from the default pool unless a specific pool has been given for a type. But there are two exceptions, one concerns access parameter allocation and the other concerns coextensions; in these cases allocation uses a pool that depends upon the context.
Thus in the case of the procedure Proc discussed above, a call such as
P.Proc(new Integer'(10));
might allocate the space in a secret pool created on the fly and that secret pool might be placed on the stack.
Such allocation can be prevented by two more specific restrictions. They are
pragma Restriction(No_Access_Parameter_Allocators);}
pragma Restriction(No_Coextensions);
These two pragmas plus using the restriction Default_Storage_Pool with null ensure that all allocation is from user-defined pools.

Contents   Index   References   Search   Previous   Next 
© 2011, 2012, 2013 John Barnes Informatics.
Sponsored in part by:
The Ada Resource Association:


and   Ada-Europe: