Limited Types
Assignment and equality
Todo
Complete section!
Limited types as parameter
Todo
Complete section!
Initialization and function return
Todo
Complete section!
Limited record elements
Todo
Complete section!
Private implementation of limited types
Todo
Complete section!
Limited types and aggregates
Note
This section was originally written by Robert A. Duff and published as Gem #1: Limited Types in Ada 2005, Gem #2, and Gem #3.
Full coverage rules
One interesting feature of Ada is the full coverage rules for aggregates. For example, suppose we have a record type:
We can create an object of the type using an aggregate:
The full coverage rules say that every component of Person
must be
accounted for in the aggregate. If we later modify type Person
by
adding a component:
and we forget to modify X
accordingly, the compiler will remind us.
Case statements also have full coverage rules, which serve a similar
purpose.
Of course, we can defeat the full coverage rules by using others
(usually for array aggregates and case statements, but occasionally useful
for record aggregates):
According to the Ada RM, others
here means precisely the same thing
as Age | Shoe_Size
. But that's wrong: what others
really
means is "all the other components, including the ones we might add next
week or next year". That means you shouldn't use others
unless
you're pretty sure it should apply to all the cases that haven't been
invented yet.
Full coverage rules for limited types
The full coverage rules have been aiding maintenance since Ada 83. Since Ada 2005, however, we can also use them for limited types. Suppose we have the following limited type:
This type has a self-reference; it doesn't make sense to copy objects,
because Self
would end up pointing to the wrong place. Therefore,
we would like to make the type limited, to prevent developers from
accidentally making copies. After all, the type is probably private, so
developers using this package might not be aware of the problem. We could
also solve that problem with controlled types, but controlled types are
expensive, and add unnecessary complexity if not needed.
Prior to Ada 2005, aggregates were illegal for limited types. Therefore, we would be faced with a difficult choice: Make the type limited, and initialize it like this:
which has the maintenance problem the full coverage rules are supposed to prevent. Or, make the type non-limited, and gain the benefits of aggregates, but lose the ability to prevent copies.
Since Ada 2005, an aggregate is allowed to be limited; we can say:
We'll see what to do about that Self => null
soon.
One very important requirement should be noted: the implementation is
required to build the value of X
in place; it cannot construct
the aggregate in a temporary variable and then copy it into X
,
because that would violate the whole point of limited objects —
you can't copy them.
It seems uncomfortable to set the value of Self
to the wrong value
(null
) and then correct it. It also seems annoying that we have a
(correct) default value for Self
, but prior to Ada 2005, we
couldn't use defaults with aggregates. Since Ada 2005, a new syntax in
aggregates is available: <>
means "use the default value, if any".
Here, we can say:
The Self => <>
means use the default value of
Limited_Person'Unchecked_Access
. Since Limited_Person
appears inside the type declaration, it refers to the "current instance"
of the type, which in this case is X
. Thus, we are setting
X.Self
to be X'Unchecked_Access
.
Note that using <>
in an aggregate can be dangerous, because it can
leave some components uninitialized. <>
means "use the default
value". If the type of a component is scalar, and there is no
record-component default, then there is no default value.
For example, if we have an aggregate of type String
, like this:
we end up with a 10-character string all of whose characters are invalid values. Note that this is no more nor less dangerous than this:
As always, one must be careful about uninitialized scalar objects.
Constructor functions for limited types
Given that we can use build-in-place aggregates for limited types, the obvious next step is to allow such aggregates to be wrapped in an abstraction — namely, to return them from functions. After all, interesting types are usually private, and we need some way for clients to create and initialize objects.
Prior to Ada 2005, constructor functions (that is, functions that create new objects and return them) were not allowed for limited types. Since Ada 2005, fully-general constructor functions are allowed. Given the above, clients can say:
As for aggregates, the result of Make_T
is built in place (that is,
in My_T
), rather than being created and then copied into
My_T
. Adding another level of function call, we can do:
It might help to understand the implementation model: In this case,
Rumplestiltskin_Is_My_Name
is allocated in the usual way (on the
stack, presuming it is declared local to some subprogram). Its address is
passed as an extra implicit parameter to Make_Rumplestiltskin
,
which then passes that same address on to Make_T
, which then builds
the aggregate in place at that address. Limited objects must never be
copied! In this case, Make_T
will initialize the Name
component, and create the My_Task
and My_Prot
components,
all directly in Rumplestiltskin_Is_My_Name
.
Note that Rumplestiltskin_Is_My_Name
is constant. Prior to
Ada 2005, it was impossible to create a constant limited object, because
there was no way to initialize it.
The (<>)
on type T
means that it has unknown
discriminants from the point of view of the client. This is a trick that
prevents clients from creating default-initialized objects (that is,
X : T;
is illegal). Thus clients must call Make_T
whenever
an object of type T
is created, giving package P
full
control over initialization of objects.
Ideally, limited and non-limited types should be just the same, except for the essential difference: you can't copy limited objects. By allowing functions and aggregates for limited types, we're very close to this goal. Some languages have a specific feature called constructor. In Ada, a constructor is just a function that creates a new object. Prior to Ada 2005, that only worked for non-limited types. For limited types, the only way to construct on declaration was via default values, which limits you to one constructor. And the only way to pass parameters to that construction was via discriminants. Consider the following package:
Since Ada 2005, we can say:
whether or not Set
is limited. This_Set : Set := Empty_Set;
seems clearer than:
which might mean "default-initialize to the empty set" or might mean "leave it uninitialized, and we'll initialize it in later".
Return objects
Note
This section was originally written by Robert A. Duff and published as Gem #10: Limited Types in Ada 2005.
Extended return statements for limited types
Previously, we discussed extended return statements. For most types, extended return statements are no big deal — it's just syntactic sugar. But for limited types, this syntax is almost essential:
The return statement here is illegal, because Result
is local to
Make_Task
, and returning it would involve a copy, which makes no
sense (which is why task types are limited). Since Ada 2005, we can write
constructor functions for task types:
If we call it like this:
Result
is created in place in My_Task
. Result
is
temporarily considered local to Make_Task
during the
-- some statements
part, but as soon as Make_Task
returns,
the task becomes more global. Result
and My_Task
really are
one and the same object.
When returning a task from a function, it is activated after the function
returns. The -- some statements
part had better not try to call one
of the task's entries, because that would deadlock. That is, the entry
call would wait until the task reaches an accept statement, which will
never happen, because the task will never be activated.
Other usages of extended return statements
While the extended_return_statement
was added to the language
specifically to support limited constructor functions, it comes in handy
whenever you want a local name for the function result:
Building objects from constructors
Note
This section was originally written by Robert A. Duff and published as Gem #11: Limited Types in Ada 2005.
We've earlier seen examples of constructor functions for limited types similar to this:
It is useful to consider the various contexts in which these functions may be called. We've already seen things like:
in which case the limited object is built directly in a standalone object. This object will be finalized whenever the surrounding scope is left.
We can also do:
Here, the result of the function is built directly in the formal parameter
X
of Do_Something
. X
will be finalized as soon as we
return from Do_Something
.
We can allocate initialized objects on the heap:
The result of the function is built directly in the heap-allocated object,
which will be finalized when the scope of T_Ref
is left (long after
Heap_Alloc
returns).
We can create another limited type with a component of type T
, and
use an aggregate:
As usual, the function results are built in place, directly in
Outer_Obj.This
and Outer_Obj.That
, with no copying involved.
The one case where we cannot call such constructor functions is in an assignment statement:
which is illegal because assignment statements involve copying. Likewise, we can't copy a limited object into some other object:
Default initialization
Note
This section was originally written by Robert A. Duff and published as Gem #12: Limited Types in Ada 2005.
Prior to Ada 2005, the following style was common:
We want Object_100
to be a default-initialized T
, with
Count
equal to 100
. It's a little bit annoying that we had
to write the default values Red
and False
twice. What if we
change our mind about Red
, and forget to change it in all the
relevant places?
Since Ada 2005, the <>
notation comes to the rescue. If we want to
say, "make Count
equal 100
, but initialize Color
and
Is_Gnarly
to their defaults", we can do this:
On the other hand, if we want to say, "make Count
equal 100
,
but initialize all other components, including the ones we might add next
week, to their defaults", we can do this:
Note that if we add a component Glorp : Integer;
to type T
,
then the others
case leaves Glorp
undefined just as this
code would do:
Therefore, you should be careful and think twice before using
others
.