Rationale for Ada 2005
4.5 Limited types and return statements
The general idea of a limited type is to restrict
the operations that a user can do on the type to just those provided
by the author of the type and in particular to prevent the user from
doing assignment and thus making copies of objects of the type.
However, limited types have always been a problem.
In Ada 83 the concept of limitedness was confused with that of private
types. Thus in Ada 83 we only had limited private types (although task
types were inherently limited).
Ada 95 brought significant improvement by two changes.
It allowed limitedness to be separated from privateness. It also allowed
the redefinition of equality for all types whereas Ada 83 forbade this
for limited types. In Ada 95, the key property of a limited type is that
assignment is not predefined and cannot be defined (equality is not predefined
either but it can be defined). The general idea of course is that there
are some types for which it would be wrong for the user to be able to
make copies of objects. This particularly applies to types involved in
resource control and types implemented using access types.
However, although Ada 95 greatly improved the situation
regarding limited types, nevertheless two major difficulties have remained.
One concerns the initialization of objects and the other concerns the
results of functions.
The first problem is that Ada 95 treats initialization
as a process of assigning the initial value to the object concerned (hence
the use of
:= unlike some Algol based languages
which use
= for initialization and
:=
for assignment). And since initialization is treated as assignment it
is forbidden for limited types. This means that we cannot initialize
objects of a limited type nor can we declare constants of a limited
type. We cannot declare constants because they have to be initialized
and yet initialization is forbidden. This is more annoying in Ada 95
since we can make a type limited but not private.
The following example
was discussed in the Introduction
type T is limited
record
A: Integer;
B: Boolean;
C: Float;
end record;
Note that this type
is explicitly limited (but not private) but its components are not limited.
If we declare an object of type T in Ada 95
then we have to initialize the components (by assigning to them) individually
thus
X: T;
begin
X.A := 10; X.B := True; X.C := 45.7;
Not only is this annoying but it is prone to errors
as well. If we add a further component D to
the type T then we might forget to initialize
it. One of the advantages of aggregates is that we have to supply all
the components which automatically provides full coverage analysis.
This problem did not arise in Ada 83 because we could
not make a type limited without making it also private and so the individual
components were not visible anyway.
Ada 2005 overcomes
the difficulty by stating that initialization by an aggregate is not
actually assignment even though depicted by the same symbol. This permits
X: T := (A => 10, B => True, C => 45.7);
We should think of the individual components as being
initialized individually in situ – an actual aggregated
value is not created and then assigned.
The reader might recall that the same thing happens
when an aggregate is used to initialize a controlled type; this was not
as Ada 95 was originally defined but it was corrected in
AI-83
and consolidated in the 2001 Corrigendum
[2].
We can now declare
a constant of a limited type as expected
X: constant T := (A => 10, B => True, C => 45.7);
Limited aggregates
can be used in a number of other contexts as well
- as the default expression in a component
declaration,
so if we nest the
type T inside some other type (which itself
then is always limited – it could be explicitly limited but there
is a general rule that a type is implicitly limited if it has a limited
component) we might have
type Twrapper is
record
Tcomp: T := (0, False, 0.0);
end record;
- as an expression in a record aggregate,
so again using the
type Twrapper as in
XT: Twrapper := (Tcomp => (1, True, 1.0));
- as an expression in an array aggregate
similarly,
type Tarr is array (1 .. 5) of T;
Xarr: Tarr := (1 .. 5 => (2, True, 2.0));
- as the expression for the ancestor
part of an extension aggregate,
so if TT
were tagged as in
type TT is tagged limited
record
A: Integer;
B: Boolean;
C: Float;
end record;
type TTplus is new TT with
record
D: Integer;
end record;
...
XTT: TTplus := ((1, True, 1.0) with 2);
- as the expression in an initialized
allocator,
type T_Ptr is access T;
XT_Ptr: T_Ptr;
...
XT_Ptr := new T'(3, False, 3.0);
- as the
actual parameter for a subprogram parameter of a limited type of mode
in
procedure P(X: in T);
...
P((4, True, 4.0));
- similarly
as the default expression for a parameter
procedure P(X: in T := (4, True, 4.0));
- as the
result in a return statement
function F( ... ) return T is
begin
...
return (5, False, 5.0);
end F;
this really concerns the other major change to limited
types which we shall return to in a moment.
- as the
actual parameter for a generic formal limited object parameter of mode
in,
generic
FT: in T;
package P is ...
...
package Q is new P(FT => (7, True, 7.0));
The last example is
interesting. Limited generic parameters were not allowed in Ada 95 at
all because there was no way of passing an actual parameter because the
generic parameter mechanism for an in parameter is considered to be assignment.
But now the actual parameter can be passed as an aggregate. An aggregate
can also be used as a default value for the parameter thus
generic
FT: in T := (0, False, 0.0);
package P is ...
Remember that there is a difference between subprogram
and generic parameters. Subprogram parameters were always allowed to
be of limited types since they are mostly implemented by reference and
no copying happens anyway. The only exception to this is with limited
private types where the full type is an elementary type.
The change in Ada 2005 is that an aggregate can be
used as the actual parameter in the case of a subprogram parameter of
mode in whereas that was not possible in Ada 95.
Sometimes a limited
type has components where an initial value cannot be given as in
protected type Semaphore is ... ;
type PT is
record
Guard: Semaphore;
Count: Integer;
Finished: Boolean := False;
end record;
Since a protected type
is inherently limited the type PT is also
limited because a type with a limited component is itself limited. Although
we cannot give an explicit initial value for a Semaphore,
we would still like to use an aggregate to get the coverage check. In
such cases we can use the box symbol <>
as described in the previous section to mean use the default value for
the type (if any). So we can write
X: PT := (Guard => <>, Count => 0, Finished => <>);
The major rule that
must always be obeyed is that values of limited types can never be copied.
Consider nested limited types
type Inner is limited
record
L: Integer;
M: Float;
end record;
type Outer is limited
record
X: Inner;
Y: Integer;
end record;
If we declare an object
of type Inner
An_Inner: Inner := (L => 2, M => 2.0);
then we could not use
An_Inner in an aggregate of type Outer
An_Outer: Outer := (X => An_Inner, Y => 3); -- illegal
This is illegal because
we would be copying the value. But we can use a nested aggregate as mentioned
earlier
An_Outer: Outer := (X => (2, 2.0), Y => 3);
The other major change to limited types concerns
returning values from functions.
We have seen that the ability to initialize an object
of a limited type with an aggregate solves the problem of giving an initial
value to a limited type provided that the type is not private.
Ada 2005 introduces a new approach to returning the
results from functions which can be used to solve this and other problems.
We will first consider
the case of a type that is limited such as
type T is limited
record
A: Integer;
B: Boolean;
C: Float;
end record;
We can declare a function
that returns a value of type T provided that
the return does not involve any copying. For example we could have
function Init(X: Integer; Y: Boolean; Z: Float) return T is
begin
return (X, Y, Z);
end Init;
This function builds
the aggregate in place in the return expression and delivers it to the
location specified where the function is called. Such a function can
be called from precisely those places listed above where an aggregate
can be used to build a limited value in place. For example
V: T := Init(2, True, 3.0);
So the function itself builds the value in the variable
V when constructing the returned value. Hence
the address of V is passed to the function
as a sort of hidden parameter.
Of course if T
is not private then this achieves no more than simply writing
V: T := (2, True, 3.0);
But the function
Init
can be used even if the type is private. It is in effect a constructor
function for the type. Moreover, the function
Init
could be used to do some general calculation with the parameters before
delivering the final value and this brings considerable flexibility.
We noted that such
a function can be called in all the places where an aggregate can be
used and this includes in a return expression of a similar function or
even itself
function Init_True(X: Integer; Z: Float) return T is
begin
return Init(X, True, Z);
end Init_True;
It could also be used
within an aggregate. Suppose we have a function to return a value of
the limited type Inner thus
function Make_Inner(X: Integer; Y: Float) return Inner is
begin
return (X, Y);
end Make_Inner;
then not only could
we use it to initialize an object of type Inner
but we could use it in a declaration of an object of type Outer
thus
An_Inner: Inner := Make_Inner(2, 2.0);
An_Outer: Outer := (X => Make_Inner(2, 2.0), Y => 3);
In the latter case the address of the component of
An_Outer is passed as the hidden parameter
to the function Make_Inner.
Being able to use a function in this way provides
much flexibility but sometimes even more flexibility is required. New
syntax permits the final returned object to be declared and then manipulated
in a general way before finally returning from the function.
The basic structure
is
function Make( ... ) return T is
begin
...
return R: T do -- declare R to be returned
... -- here we can manipulate R in the usual way
... -- in a sequence of statements
end return;
end Make;
The general idea is that the object R
is declared and can then be manipulated in an arbitrary way before being
finally returned. Note the use of the reserved word do to introduce
the statements in much the same way as in an accept statement. The sequence
ends with end return and at this point the function passes control
back to where it was called. Note that if the function had been called
in a construction such as the initialization of an object X
of a limited type T thus
X: T := Make( ... );
then the variable R inside
the function is actually the variable X being
initialized. In other words the address of X
is passed as a hidden parameter to the function Make
in order to create the space for R. No copying
is therefore ever performed.
The sequence of statements
could have an exception handler
return R: T do
... -- statements
exception
... -- handlers
end return;
If we need local variables
within an extended return statement then we can declare an inner block
in the usual way
return R: T do
declare
... -- local declarations
begin
... -- statements
end;
end return;
The declaration of
R could have an initial value
return R: T := Init( ... ) do
...
end return;
Also, much as in an
accept statement, the do ... end return part can be omitted,
so we simply get
return R: T;
or
return R: T := Init( ... );
which is handy if we just want to return the object
with its default or explicit initial value.
Observe that extended
return statements cannot be nested but could have simple return statements
inside
return R: T := Init( ... ) do
if ... then
...
return; -- result is R
end if;
...
end return;
Note that simple return statements inside an extended
return statement do not have an expression since the result returned
is the object R declared in the extended return
statement itself.
Although extended return
statements cannot be nested there could nevertheless be several in a
function, perhaps in branches of an if statement or case statement. This
would be quite likely in the case of a type with discriminants
type Person(Sex: Gender) is ... ;
function F( ... ) return Person is
begin
if ... then
return R: Person(Sex => Male) do
...
end return;
else
return R: Person(Sex => Female) do
...
end return;
end if;
end F;
This also illustrates the important point that although
we introduced these extended return statements in the context of greater
flexibility for limited types they can be used with any types at all
such as the nonlimited type Person. The mechanism
of passing a hidden parameter which is the address for the returned object
of course only applies to limited types. In the case of nonlimited types,
the result is simply delivered in the usual way.
We can also rename the result of a function call
– even if it is limited.
The result type of
a function can be constrained or unconstrained as in the case of the
type Person but the actual object delivered
must be of a definite subtype. For example suppose we have
type UA is array (Integer range <>) of Float;
subtype CA is UA(1 .. 10);
Then the type UA is unconstrained
but the subtype CA is constrained. We can
use both with extended return statements.
In the constrained
case the subtype in the extended return statement has to statically match
(typically it will be the same textually but need not) thus
function Make( ... ) return CA is
begin
...
return R: UA(1 .. 10) do -- statically matches
...
end return;
end Make;
In the unconstrained
case the result R has to be constrained either
by its subtype or by its initial value. Thus
function Make( ... ) return UA is
begin
...
return R: UA(1 .. N) do
...
end return;
end Make;
or
function Make( ... ) return UA is
begin
...
return R: UA := (1 .. N => 0.0) do
...
end return;
end Make;
The other important
change to the result of functions which was discussed in the previous
chapter (see
3.3) is that the result type
can be of an anonymous access type. So we can write a function such as
function Mate_Of(A: access Animal'Class) return access Animal'Class;
The introduction of explicit access types for the
result means that Ada 2005 is able to dispense with the notion of returning
by reference.
This does, however,
introduce a noticeable incompatibility between Ada 95 and Ada 2005. We
might for example have a pool of slave tasks acting as servers. Individual
slave tasks might be busy or idle. We might have a manager task which
allocates slave tasks to different jobs. The manager might declare the
tasks as an array
Slaves: array (1 .. 10) of TT; -- TT is some task type
and then have another
array of properties of the tasks such as
type Task_Data is
record
Active: Boolean := False;
Job_Code: ... ;
end record;
Slave_Data: array (1 .. 10) of Task_Data;
We now need a function
to find an available slave. In Ada 95 we write
function Get_Slave return TT is
begin
... -- find index K of first idle slave
return Slaves(K); -- in Ada 95, not in Ada 2005
end Get_Slave;
This is not permitted in Ada 2005. If the result
type is limited (as in this case) then the expression in the return statement
has to be an aggregate or function call and not an object such as Slaves(K).
In Ada 2005 the function has to be rewritten to honestly
return an access value referring to the task type rather than invoking
the mysterious concept of returning by reference.
So we have to write
function Get_Slave return access TT is
begin
... -- find index K of first idle slave
return Slaves(K)'Access; -- in Ada 2005
end Get_Slave;
and all the calls of Get_Slave
have to be changed to correspond as well.
This is perhaps the most serious incompatibility
between Ada 95 and Ada 2005. But then, at the end of the day, honesty
is the best policy.
© 2005, 2006 John Barnes Informatics.
Sponsored in part by: