Rationale for Ada 2005

John Barnes
Contents   Index   References   Search   Previous   Next 

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.

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