Rationale for Ada 2005
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_access) return Boolean;
function "/=" (Left, Right: universal_access) return 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, null) or Standard."=" (R, null) then
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 a later
chapter (see
6.3).
Another example of
the use of the type
Cell occurred in the previous
chapter (see
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 R 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.
© 2005, 2006 John Barnes Informatics.
Sponsored in part by: