Rationale for Ada 2012
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
record
...
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;
begin
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
record
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
else
... -- 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
record
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;
Size_In_Storage_Elements;
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
begin
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;
private
...
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;
overriding
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);
overriding
procedure Deallocate( ... ) is null;
overriding
function Storage_Size (Pool : Root_Storage_Pool_With_Subpools)
return Storage_Count is
(Storage_Count'Last);
private
... -- 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
Unchecked_Dellocate_Subpool(Puddle);
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);}
and
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.
© 2011, 2012, 2013 John Barnes Informatics.
Sponsored in part by: