Rationale for Ada 2005
3.2 Null exclusion and constant
In Ada 95, anonymous access types and named access
types have unnecessarily different properties. Furthermore anonymous
access types only occur as access parameters and access discriminants.
Anonymous access types
in Ada 95 never have null as a value whereas named access types always
have null as a value. Suppose we have the following declarations
type T is
record
Component: Integer;
end record;
type Ref_T is access T;
T_Ptr: Ref_T;
Note that T_Ptr
by default will have the value null. Now suppose we have a procedure
with an access parameter thus
procedure P(A: access T) is
X: Integer;
begin
X := A.Component; -- read a component of A
-- no check for null in Ada 95
...
end P;
In Ada 95 an access parameter such as A
can never have the value null and so there is no need to check for null
when doing a dereference such as reading the component A.Component.
This is assured by always performing a check when P
is called. So calling P with an actual parameter
whose value is null such as P(T_Ptr) causes
Constraint_Error to be raised at the point
of call. The idea was that within P we would
have more efficient code for dereferencing and dispatching at the cost
of just one check when the procedure is called. Such an access parameter
we now refer to as being of a subtype that excludes null.
Ada 2005 extends this
idea of access types that exclude null to named access types as well.
Thus we can write
type Ref_NNT is not null access T;
In this case an object of the type Ref_NNT
cannot have the value null. An immediate consequence is that all such
objects should be explicitly initialized – they will otherwise
be initialized to null by default and this will raise Constraint_Error.
Since the property
of excluding null can now be given explicitly for named types, it was
decided that for uniformity, anonymous access types should follow the
same rule whenever possible. So, if we want an access parameter such
as A to exclude null in Ada 2005 then we have
to indicate this in the same way
procedure PNN(A: not null access T) is
X: Integer;
begin
X := A.Component; -- read a component of A
-- no check for null in Ada 2005
...
end PNN;
This means of course
that the original procedure
procedure P(A: access T) is
X: Integer;
begin
X := A.Component; -- read a component of A
-- check for null in Ada 2005
...
end P;
behaves slightly differently in Ada 2005 since A
is no longer of a type that excludes null. There now has to be a check
when accessing the component of the record because null is now an allowed
value of A. So in Ada 2005, calling P
with a null parameter results in Constraint_Error
being raised within P only when we attempt
to do the dereference, whereas in Ada 95 it is always raised at the point
of call.
This is of course technically an incompatibility
of an unfortunate kind. Here we have a program that is legal in both
Ada 95 and Ada 2005 but it behaves differently at execution time in that
Constraint_Error is raised at a different
place. But of course, in practice if such a program does raise Constraint_Error
in this way then it clearly has a bug and so the difference does not
really matter.
Various alternative approaches were considered in
order to eliminate this incompatibility but they all seemed to be ugly
and it was felt that it was best to do the proper thing rather than have
a permanent wart.
However the situation
regarding controlling access parameters is somewhat different. Remember
that a controlling parameter is a parameter of a tagged type where the
operation is primitive – that is declared alongside the tagged
type in a package specification (or inherited of course). Thus consider
package PTT is
type TT is tagged
record
Component: Integer;
end record;
procedure Op(X: access TT); -- primitive operation
...
end PTT;
The type TT is tagged
and the procedure Op is a primitive operation
and so the access parameter X is a controlling
parameter.
In this case the anonymous access (sub)type still
excludes null as in Ada 95 and so null is not permitted as a parameter.
The reason is that controlling parameters provide the tag for dispatching
and null has no tag value. Remember that all controlling parameters have
to have the same tag. We can add not null to the parameter specification
if we wish but to require it explicitly for all controlling parameters
was considered to be too much of an incompatibility. But in newly written
programs, we should be encouraged to write not null explicitly
in order to avoid confusion during maintenance.
Another rule regarding
null exclusion is that a type derived from a type that excludes null
also excludes null. Thus given
type Ref_NNT is not null access T;
type Another_Ref_NNT is new Ref_NNT;
then Another_Ref_NNT also excludes null. On the other hand if we start
with an access type that does not exclude null then a derived type can
exclude null or not thus
type Ref_T is access T;
type Another_Ref_T is new Ref_T;
type ANN_Ref_T is new not null Ref_T;
then Another_Ref_T does
not exclude null but ANN_Ref_T does exclude
null.
A technical point is that all access types including
anonymous access types in Ada 2005 have null as a value whereas in Ada
95 the anonymous access types did not. It is only subtypes in Ada 2005
that do not always have null as a value. Remember that Ref_NNT
is actually a first-named subtype.
An important advantage of all access types having
null as a value is that it makes interfacing to C much easier. If a parameter
in C has type *t then the corresponding parameter
in Ada can have type access T and if
the C routine needs null passed sometimes then all is well – this
was a real pain in Ada 95.
An explicit null exclusion
can also be used in object declarations much like a constraint. Thus
we can have
type Ref_Int is access all Integer;
X: not null Ref_Int := Some_Integer'Access;
Note that we must initialize X
otherwise the default initialization with null will raise Constraint_Error.
In some ways null exclusions
have much in common with constraints. We should compare the above with
Y: Integer range 1 .. 10;
...
Y := 0;
Again Constraint_Error
is raised because the value is not permitted for the subtype of Y.
A difference however is that in the case of X
the check is Access_Check whereas in the case
of Y it is Range_Check.
The fact that a null
exclusion is not actually classified as a constraint is seen by the syntax
for subtype_indication which in Ada 2005 is
subtype_indication ::= [null_exclusion] subtype_mark [constraint]
An explicit null exclusion
can also be used in subprogram declarations thus
function F(X: not null Ref_Int) return not null Ref_Int;
procedure P(X: in not null Ref_Int);
procedure Q(X: in out not null Ref_Int);
But a difference between
null exclusions and constraints is that although we can use a null exclusion
in a parameter specification we cannot use a constraint in a parameter
specification. Thus
procedure P(X: in not null Ref_Int); -- legal
procedure Q(X: in Integer range 1 .. N); -- illegal
But null exclusions are like constraints in that
they are both used in defining subtype conformance and static matching.
We can also use a null
exclusion with access-to-subprogram types including protected subprograms.
type F is access function (X: Float) return Float;
Fn: not null F := Sqrt'Access;
and so on.
A null exclusion can also be used in object and subprogram
renamings. We will consider subprogram renamings here and object renamings
in the next section when we discuss anonymous access types. This is an
area where there is a significant difference between null exclusions
and constraints.
Remember that if an
entity is renamed then any constraints are unchanged. We might have
procedure P(X: Positive);
...
procedure Q(Y: Natural) renames P;
...
Q(0); -- raises Constraint_Error
The call of Q raises Constraint_Error
because zero is not an allowed value of Positive.
The constraint Natural on the renaming is
completely ignored (Ada has been like that since time immemorial).
We would have preferred that this sort of peculiar
behaviour did not extend to null exclusions. However, we already have
the problem that a controlling parameter always excludes null even if
it does not say so. So the rule adopted generally with null exclusions
is that "null exclusions never lie". In other words, if we
give a null exclusion then the entity must exclude null; however, if
no null exclusion is given then the entity might nevertheless exclude
null for other reasons (as in the case of a controlling parameter).
So consider
procedure P(X: not null access T);
...
procedure Q(Y: access T) renames P; -- OK
...
Q(null); -- raises Constraint_Error
The call of Q
raises Constraint_Error because the parameter
excludes null even though there is no explicit null exclusion in the
renaming. On the other hand (we assume that X
is not a controlling parameter)
procedure P(X: access T);
...
procedure Q(Y: not null access T) renames P; -- NO
is illegal because the null exclusion in the renaming
is a lie.
However, if P had been
a primitive operation of T so that X
was a controlling parameter then the renaming with the null exclusion
would be permitted.
Care needs to be taken
when a renaming itself is used as a primitive operation. Consider
package P is
type T is tagged ...
procedure One(X: access T); -- excludes null
package Inner is
procedure Deux(X: access T); -- includes null
procedure Trois(X: not null access T); -- excludes null
end Inner;
use Inner;
procedure Two(X: access T) renames Deux; -- NO
procedure Three(X: access T) renames Trois; -- OK
...
The procedure One is a
primitive operation of T and its parameter
X is therefore a controlling parameter and
so excludes null even though this is not explicitly stated. However,
the declaration of Two is illegal. It is trying
to be a dispatching operation of T and therefore
its controlling parameter X has to exclude
null. But Two is a renaming of Deux
whose corresponding parameter does not exclude null and so the renaming
is illegal. On the other hand the declaration of Three
is permitted because the parameter of Trois
does exclude null.
The other area that
needed unification concerned
constant. In Ada 95 a named access
type can be an access to constant type rather than an access to variable
type thus
type Ref_CT is access constant T;
Remember that this means that we cannot change the
value of an object of type T via the access
type.
Remember also that
Ada 95 introduced more general access types whereas in Ada 83 all access
types were pool specific and could only access values created by an allocator.
An access type in Ada 95 can also refer to any object marked aliased
provided that the access type is declared with all thus
type Ref_VT is access all T;
X: aliased T;
R: Ref_VT := X'Access;
So in summary, Ada
95 has three kinds of named access types
access T; -- pool specific only, read & write
access all T -- general, read & write
access constant T -- general, read only
But in Ada 95, the
distinction between variable and constant access parameters is not permitted.
Ada 2005 rectifies this by permitting constant with access parameters.
So we can write
procedure P(X: access constant T); -- legal Ada 2005
procedure P(X: access T);
Observe however, that
all is not permitted with access parameters. Ordinary objects
can be constant or variable thus
C: constant Integer := 99;
V: Integer;
and access parameters follow this pattern. It is
named access types that are anomalous because of the need to distinguish
pool specific types for compatibility with Ada 83 and the subsequent
need to introduce all.
In summary, Ada 2005
access parameters can take the following four forms
procedure P1(X: access T);
procedure P2(X: access constant T);
procedure P3(X: not null access T);
procedure P4(X: not null access constant T);
Moreover, as mentioned above, controlling parameters
always exclude null even if this is not stated and so in that case P1
and P3 are equivalent. Controlling parameters
can also be constant in which case P2 and
P4 are equivalent.
Similar rules apply to access discriminants; thus
they can exclude null and/or be access to constant.
© 2005, 2006, 2007 John Barnes Informatics.
Sponsored in part by: