Rationale for Ada 2005

John Barnes
Contents   Index   References   Search   Previous   Next 

3.3 Anonymous access types

As just mentioned, Ada 95 permits anonymous access types only as access parameters and access discriminants. And in the latter case only for limited types. Ada 2005 sweeps away these restrictions and permits anonymous access types quite freely.
The main motivation for this change concerns type conversion. It often happens that we have a type T somewhere in a program and later discover that we need an access type referring to T in some other part of the program. So we introduce 
type Ref_T is access all T;
And then we find that we also need a similar access type somewhere else and so declare another access type 
type T_Ptr is access all T;
If the uses of these two access types overlap then we will find that we have explicit type conversions all over the place despite the fact that they are really the same type. Of course one might argue that planning ahead would help a lot but, as we know, programs often evolve in an unplanned way.
A more important example of the curse of explicit type conversion concerns object oriented programming. Access types feature quite widely in many styles of OO programming. We might have a hierarchy of geometrical object types starting with a root abstract type Object thus 
type Object is abstract;
type Circle is new Object with ...
type Polygon is new Object with ...
type Pentagon is new Polygon with ...
type Triangle is new Polygon with ...
type Equilateral_Triangle is new Triangle with ...
then we might well find ourselves declaring named access types such as 
type Ref_Object is access all Object'Class;
type Ref_Circle is access all Circle;
type Ref_Triangle is access all Triangle'Class;
type Ref_Equ_Triangle is access all Equilateral_Triangle;
Conversion between these clearly ought to be permitted in many cases. In some cases it can never go wrong and in others a run time check is required. Thus a conversion between a Ref_Circle and a Ref_Object is always possible because every value of Ref_Circle is also a value of Ref_Object but the reverse is not the case. So we might have 
RC: Ref_Circle := A_Circle'Access;
RO: Ref_Object;
...
RO := Ref_Object(RC);    -- explicit conversion, no check
...
RC := Ref_Circle(RO);    -- needs a check
However, it is a rule of Ada 95 that type conversions between these named access types have to be explicit and give the type name. This is considered to be a nuisance by many programmers because such conversions are allowed without naming the type in other OO languages. It would not be quite so bad if the explicit conversion were only required in those cases where a run time check was necessary.
Moreover, these are trivial (view) conversions since they are all just pointers and no actual change of value takes place anyway; all that has to be done is to check that the value is a legal reference for the target type and in many cases this is clear at compilation. So requiring the type name is very annoying.
In fact the only conversions between named tagged types (and named access types) that are allowed implicitly in Ada are conversions to a class wide type when it is initialized or when it is a parameter (which is really the same thing).
It would have been nice to have been able to relax the rules in Ada 2005 perhaps by saying that a named conversion is only required when a run time check is required. However, such a change would have caused lots of existing programs to become ambiguous.
So, rather than meddle with the conversion rules, it was instead decided to permit the use of anonymous access types in more contexts in Ada 2005. Anonymous access types have the interesting property that they are anonymous and so necessarily do not have a name that could be used in a conversion. Thus we can have
RC: access Circle := A_Circle'Access;
RO: access Object'Class;    -- default null
...
RO := RC;    -- implicit conversion, no check
On the other hand we cannot write 
RC := RO;    -- illegal, would need a check
because the general rule is that if a tag check is required then the conversion must be explicit. So typically we will still need to introduce named access types for some conversions. But checks relating to accessibility and null exclusions do not require an explicit conversion and so anonymous access types cause no problems in those areas.
The use of null exclusions with anonymous access types is illustrated by 
RC: not null access Circle := A_Circle'Access;
RO: not null access Object'Class;    --careful
The declaration of RO is unfortunate because no initial value is given and the default of null is not permitted and so it will raise Constraint_Error; a worthy compiler will detect this during compilation and give us a friendly warning.
Note that we never never write all with anonymous access types.
We can of course also use constant with anonymous access types. Note carefully the difference between the following 
ACT: access constant T := T1'Access;
CAT: constant access T := T1'Access;
In the first case ACT is a variable and can be used to access different objects T1 and T2 of type T. But it cannot be used to change the value of those objects. In the second case CAT is a constant and can only refer to the object given in its initialization. But we can change the value of the object that CAT refers to. So we have
ACT := T2'Access;    -- legal, can assign
ACT.all := T2;    -- illegal, constant view
CAT := T2'Access;    -- illegal, cannot assign
CAT.all := T2;    -- legal, variable view
At first sight this may seem confusing and consideration was given to disallowing the use of constants such as CAT (but permitting ACT which is probably more useful since it protects the accessed value). But the lack of orthogonality was considered very undesirable. Moreover Ada is a left to right language and we are familiar with equivalent constructions such as 
type CT is access constant T;
ACT: CT;
and 
type AT  is access T;
CAT: constant AT;
(although the alert reader will note that the latter is illegal because I have foolishly used the reserved word at as an identifier).
We can of course also write 
CACT: constant access constant T := T1'Access;
The object CACT is then a constant and provides read-only access to the object T1 it refers to. It cannot be changed to refer to another object such as T2 nor can the value of T1 be changed via CACT.
An object of an anonymous access type, like other objects, can also be declared as aliased thus
X: aliased access T;
although such constructions are likely to be used rarely.
Anonymous access types can also be used as the components of arrays and records. In the Introduction we saw that rather than having to write 
type Cell;
type Cell_Ptr is access Cell;
type Cell is
   record
      Next: Cell_Ptr;
      Value: Integer;
   end record;
we can simply write
type Cell is
   record
      Next: access Cell;
      Value: Integer;
   end record;
and this not only avoids having to declare the named access type Cell_Ptr but it also avoids the need for the incomplete type declaration of Cell.
Permitting this required some changes to a rule regarding the use of a type name within its own declaration – the so-called current instance rule.
The original current instance rule was that within a type declaration the type name did not refer to the type itself but to the current object of the type. The following task type declaration illustrates both a legal and illegal use of the task type name within its own declaration. It is essentially an extract from a program in Section 18.10 of [6] which finds prime numbers by a multitasking implementation of the Sieve of Eratosthenes. Each task of the type is associated with a prime number and is responsible for removing multiples of that number and for creating the next task when a new prime number is discovered. It is thus quite natural that the task should need to make a clone of itself.
task type TT (P: Integer) is
   ...
end;
type ATT is access TT;
task body TT is
   function Make_Clone(N: Integer) return ATT is
   begin
      return new TT(N);    -- illegal
   end Make_Clone;
   Ref_Clone: ATT;
   ...
begin
   ...
   Ref_Clone := Make_Clone(N);
   ...
   abort TT;    -- legal
   ...
end TT;
The attempt to make a slave clone of the task in the function Make_Clone is illegal because within the task type its name refers to the current instance and not to the type. However, the abort statement is permitted and will abort the current instance of the task. In this example the solution is simply to move the function Make_Clone outside the task body.
However, this rule would have prevented the use of the type name Cell to declare the component Next within the type Cell and this would have been infuriating since the linked list paradigm is very common.
In order to permit this the current instance rule has been changed in Ada 2005 to allow the type name to denote the type itself within an anonymous access type declaration (but not a named access type declaration). So the type Cell is permitted.
Note however that in Ada 2005, the task TT still cannot contain the declaration of the function Make_Clone. Although we no longer need to declare the named type ATT since we can now declare Ref_Clone as 
Ref_Clone: access TT;
and we can declare the function as 
   function Make_Clone(N: Integer) return access TT is
   begin
      return new TT(N);
   end Make_Clone;
where we have an anonymous result type, nevertheless the allocator new TT inside Make_Clone remains illegal if Make_Clone is declared within the task body TT. But such a use is unusual and declaring a distinct external function is hardly a burden.
To be honest we can simply declare a subtype of a different name outside the task 
subtype XTT is TT;
and then we can write new XTT(N); in the function and keep the function hidden inside the task. Indeed we don't need the function anyway because we can just write 
Ref_Clone := new XTT(N);
in the task body.
The introduction of the wider use of anonymous access types requires some revision to the rules concerning type comparisons and conversions. This is achieved by the introduction of a type universal_access by analogy with the types universal_integer and universal_real. Two new equality operators are defined in the package Standard thus 
function "=" (Left, Right: universal_accessreturn Boolean;
function "/=" (Left, Right: universal_accessreturn Boolean;
The literal null is now deemed to be of type universal_access and appropriate conversions are defined as well. These new operations are only applied when at least one of the arguments is of an anonymous access type (not counting null).
Interesting problems arise if we define our own equality operation. For example, suppose we wish to do a deep comparison on two lists defined by the type Cell. We might decide to write a recursive function with specification 
function "=" (L, R: access Cell) return Boolean;
Note that it is easier to use access parameters rather than parameters of type Cell itself because it then caters naturally for cases where null is used to represent an empty list. We might attempt to write the body as 
function "=" (L, R: access Cell) return Boolean is
begin
   if L = null or R = null then    -- wrong =
      return L = R;    -- wrong =
   elsif L.Value = R.Value then
      return L.Next = R.Next;    -- recurses OK
   else
      return False;
   end if;
end "=" ;
But this doesn't work because the calls of "=" in the first two lines recursively call the function being declared whereas we want to call the predefined "=" in these cases.
The difficulty is overcome by writing Standard."=" thus 
   if Standard."=" (L, nullor Standard."=" (R, nullthen
      return Standard."=" (L, R);
The full rules regarding the use of the predefined equality are that it cannot be used if there is a user-defined primitive equality operation for either operand type unless we use the prefix Standard. A similar rule applies to fixed point types as we shall see in Section 6.3.
Another example of the use of the type Cell occurred in Section 2.5 when we were discussing type extension at nested levels. That example also illustrated that access types have to be named in some circumstances such as when they provide the full type for a private type. We had 
package Lists is
   type List is limited private;    -- private type
   ...
private
   type Cell is
      record
         Next: access Cell;    -- anonymous type
         C: Colour;
      end record;
   type List is access Cell;    -- full type
end;
package body Lists is
   procedure Iterate(IC: in Iterator'Class; L: in List) is
      This: access Cell := L;    -- anonymous type
   begin
      while This /= null loop
         IC.Action(This.C);    -- dispatches
         This := This.Next;
      end loop;
   end Iterate;
end Lists;
In this case we have to name the type List because it is a private type. Nevertheless it is convenient to use an anonymous access type to avoid an incomplete declaration of Cell.
In the procedure Iterate the local variable This is also of an anonymous type. It is interesting to observe that if This had been declared to be of the named type List then we would have needed an explicit conversion in 
         This := List(This.Next);     -- explicit conversion
Remember that we always need an explicit conversion when converting to a named access type. There is clearly an art in using anonymous types to best advantage.
The Introduction showed a number of other uses of anonymous access types in arrays and records and as function results when discussing Noah's Ark and other animal situations. We will now turn to more weighty matters.
An important matter in the case of access types is accessibility. The accessibility rules are designed to prevent dangling references. The basic rule is that we cannot create an access value if the object referred to has a lesser lifetime than the access type.
However there are circumstances where the rule is unnecessarily severe and that was one reason for the introduction of access parameters. Perhaps some recapitulation of the problems would be helpful. Consider 
type T is ...
Global: T;
type Ref_T is access all T;
Dodgy: Ref_T;
procedure P(Ptr: access T) is
begin
   ...
   Dodgy := Ref_T(Ptr);    -- dynamic check
end P;
procedure Q(Ptr: Ref_T) is
begin
   ...
   Dodgy := Ptr;    -- legal
end Q;
...
declare
   X: aliased T;
begin
   P(X'Access);    -- legal
   Q(X'Access);    -- illegal
end;
Here we have an object X with a short lifetime and we must not squirrel away an access referring to X in an object with a longer lifetime such as Dodgy. Nevertheless we want to manipulate X indirectly using a procedure such as P.
If the parameter were of a named type such as Ref_T as in the case of the procedure Q then the call would be illegal since within Q we could then assign to a variable such as Dodgy which would then retain the "address" of X after X had ceased to exist.
However, the procedure P which uses an access parameter permits the call. The reason is that access parameters carry dynamic accessibility information regarding the actual parameter. This extra information enables checks to be performed only if we attempt to do something foolish within the procedure such as make an assignment to Dodgy. The conversion to the type Ref_T in this assignment fails dynamically and disaster is avoided.
But note that if we had called P with 
P(Global'Access);
where Global is declared at the same level as Ref_T then the assignment to Dodgy would be permitted.
The accessibility rules for the new uses of anonymous access types are very simple. The accessibility level is simply the level of the enclosing declaration and no dynamic information is involved. (The possibility of preserving dynamic information was considered but this would have led to inefficiencies at the points of use.)
In the case of a stand-alone variable such as 
V: access Integer;
then this is essentially equivalent to 
type anon is access all Integer;
V: anon;
A similar situation applies in the case of a component of a record or array type. Thus if we have 
type R is
   record
      C: access Integer;
      ...
   end record;
then this is essentially equivalent to 
type anon is access all Integer;
type R is
   record
      C: anon;
      ...
   end record;
Further if we now declare a derived type then there is no new physical access definition, and the accessibility level is that of the original declaration. Thus consider
procedure Proc is
   Local: aliased Integer;
   type D is new R;
   X: D := D'(C => Local'Access, ... );     -- illegal
begin
   ...
end Proc;
In this example the accessibility level of the component C of the derived type is the same as that of the parent type and so the aggregate is illegal. This somewhat surprising rule is necessary to prevent some very strange problems which we will not explore here.
One consequence of which users should be aware is that if we assign the value in an access parameter to a local variable of an anonymous access type then the dynamic accessibility of the actual parameter will not be held in the local variable. Thus consider again the example of the procedure P containing the assignment to Dodgy 
procedure P(Ptr: access T) is
begin
   ...
   Dodgy := Ref_T(Ptr);    -- dynamic check
end P;
and this variation in which we have introduced a local variable of an anonymous access type
procedure P1(Ptr: access T) is
   Local_Ptr: access T;
begin
   ...
   Local_Ptr := Ptr;    -- implicit conversion
   Dodgy := Ref_T(Local_Ptr);    -- static check, illegal
end P1;
Here we have copied the value in the parameter to a local variable before attempting the assignment to Dodgy. (Actually it won't compile but let us analyze it in detail anyway.)
The conversion in P using the access parameter Ptr is dynamic and will only fail if the actual parameter has an accessibility level greater than that of the type Ref_T. So it will fail if the actual parameter is X and so raise Program_Error but will pass if it has the same level as the type Ref_T such as the variable Global.
In the case of P1, the assignment from Ptr to Local_Ptr involves an implicit conversion and static check which always passes. (Remember that implicit conversions are never allowed if they involve a dynamic check.) However, the conversion in the assignment to Dodgy in P1 is also static and will always fail no matter whether X or Global is passed as actual parameter.
So the effective behaviours of P and P1 are the same if the actual parameter is X (they both fail, although one dynamically and the other statically) but will be different if the actual parameter has the same level as the type Ref_T such as the variable Global. The assignment to Dodgy in P will work in the case of Global but the assignment to Dodgy in P1 never works.
This is perhaps surprising, an apparently innocuous intermediate assignment has a significant effect because of the implicit conversion and the consequent loss of the accessibility information. In practice this is very unlikely to be a problem. In any event programmers are aware that access parameters are special and carry dynamic information.
In this particular example the loss of the accessibility information through the use of the intermediate stand-alone variable is detected at compile time. More elaborate examples can be constructed whereby the problem only shows up at execution time. Thus suppose we introduce a third procedure Agent and modify P and P1 so that we have 
procedure Agent(A: access T) is
begin
   Dodgy := Ref_T(A);    -- dynamic check
end Agent;
procedure P(Ptr: access T) is
begin
   Agent(Ptr);    -- may be OK
end P;
procedure P1(Ptr: access T) is
   Local_Ptr: access T;
begin
   Local_Ptr := Ptr;    -- implicit conversion 
   Agent(Local_Ptr);    -- never OK
end P1;
Now we find that P works much as before. The accessibility level passed into P is passed to Agent which then carries out the assignment to Dodgy. If the parameter passed to P is the local X then Program_Error is raised in Agent and propagated to P. If the parameter passed is Global then all is well.
The procedure P1 now compiles whereas it did not before. However, because the accessibility of the original parameter is lost by the assignment to Local_Ptr, it is the accessibility level of Local_Ptr that is passed to Agent and this means that the assignment to Dodgy always fails and raises Program_Error irrespective of whether P1 was called with X or Global.
If we just want to use another name for some reason then we can avoid the loss of the accessibility level by using renaming. Thus we could have 
procedure P2(Ptr: access T) is
   Local_Ptr: access T renames Ptr;
begin
   ...
   Dodgy := Ref_T(Local_Ptr);    -- dynamic check
end P2;
and this will behave exactly as the original procedure P.
As usual a renaming just provides another view of the same entity and thus preserves the accessibility information.
A renaming can also include not null thus 
Local_Ptr: not null access T renames Ptr;
Remember that not null must never lie so this is only legal if Ptr is indeed of a type that excludes null (which it will be if Ptr is a controlling access parameter of the procedure P2).
A renaming might be useful when the accessed type T has components that we wish to refer to many times in the procedure. For example the accessed type might be the type Cell declared earlier in which case we might usefully have 
Next: access Cell renames Ptr.Next;
and this will preserve the accessibility information.
Anonymous access types can also be used as the result of a function. In the Introduction we had 
function Mate_Of(A: access Animal'Class) return access Animal'Class;
The accessibility level of the result in this case is the same as that of the declaration of the function itself.
We can also dispatch on the result of a function if the result is an access to a tagged type. Consider 
function Unit return access T;
We can suppose that T is a tagged type representing some category of objects such as our geometrical objects and that Unit is a function returning a unit object such as a circle of unit radius or a triangle with unit side.
We might also have a function 
function Is_Bigger(X, Y: access T) return Boolean;
and then 
Thing: access T'Class := ... ;
...
Test: Boolean := Is_Bigger(Thing, Unit);
This will dispatch to the function Unit according to the tag of Thing and then of course dispatch to the appropriate function Is_Bigger.
The function Unit could also be used as a default value for a parameter thus 
function Is_Bigger(X: access T; Y: access T := Unit)
return Boolean;
Remember that a default used in such a construction has to be tag indeterminate.
Permitting anonymous access types as result types eliminates the need to define the concept of a "return by reference" type. This was a strange concept in Ada 95 and primarily concerned limited types (including task and protected types) which of course could not be copied. Enabling us to write access explicitly and thereby tell the truth removes much confusion. Limited types will be discussed in detail in a later chapter (see 4.5).
Access return types can be a convenient way of getting a constant view of an object such as a table. We might have an array in a package body (or private part) and a function in the specification thus 
package P is
   type Vector is array (Integer range <>) of Float;
   function Read_Vec return access constant Vector;
   ...
private
end;
package body P is
   The_Vector: aliased Vector :=   ;
   function Read_Vec return access constant Vector is
   begin
      return The_Vector'Access;
   end;
   ...
end P;
We can now write 
X := Read_Vec(7);    -- read element of array
This is strictly short for 
X := Read_Vec.all(7);
Note that we cannot write 
Read_Vec(7) := Y;    -- illegal
although we could do so if we removed constant from the return type (in which case we should use a different name for the function).
The last new use of anonymous access types concerns discriminants. Remember that a discriminant can be of a named access type or an anonymous access type (as well as oher things). Discriminants of an anonymous access type are known as access discriminants. In Ada 95, access discriminants are only allowed with limited types. Discriminants of a named access type are just additional components with no special properties. But access discriminants of limited types are special. Since the type is limited, the object cannot be changed by a whole record assignment and so the discriminant cannot be changed even if it has defaults. Thus 
type Minor is ...
type Major(M: access Minor) is limited
   record
      ...
   end record;
Small: aliased Minor;
Large: Major(Small'Access);
The objects Small and Large are now bound permanently together.
In Ada 2005, access discriminants are also allowed for nonlimited types. However, defaults are not permitted so that the discriminant cannot be changed so again the objects are bound permanently together. An interesting case arises when the discriminant is provided by an allocator thus 
Larger: Major(new Minor( ... ));
In this case we say that the allocated object is a coextension of Larger. Coextensions have the same lifetime as the major object and so are finalized when it is finalized. There are various accessibility and other rules concerning objects which have coextensions which prevent difficulty when returning such objects from functions.

Contents   Index   References   Search   Previous   Next 
© 2005, 2006, 2007 John Barnes Informatics.
Sponsored in part by:
The Ada Resource Association and its member companies: ARA Members AdaCore Polyspace Technologies Praxis Critical Systems IBM Rational Sofcheck and   Ada-Europe:
Ada-Europe