Limited Types
So far, we discussed nonlimited types in most cases. In this chapter, we discuss limited types.
We can think of limited types as an easy way to avoid inappropriate semantics. For example, a lock should not be copied — neither directly, via assignment, nor with pass-by-copy. Similarly, a file, which is really a file descriptor, should not be copied. In this chapter, we'll see example of unwanted side-effects that arise if we don't use limited types for these cases.
Assignment and equality
Limited types have the following restrictions, which we discussed in the Introduction to Ada course:
copying objects of limited types via direct assignments is forbidden; and
there's no predefined equality operator for limited types.
(Of course, in the case of nonlimited types, assignments are possible and the equality operator is available.)
By having these restrictions for limited types, we avoid inappropriate side-effects for assignment and equality operations. As an example of inappropriate side-effects, consider the case when we apply those operations on record types that have components of access types:
package Nonlimited_Types is type Simple_Rec is private; type Integer_Access is access Integer; function Init (I : Integer) return Simple_Rec; procedure Set (E : Simple_Rec; I : Integer); procedure Show (E : Simple_Rec; E_Name : String); private type Simple_Rec is record V : Integer_Access; end record; end Nonlimited_Types;with Ada.Text_IO; use Ada.Text_IO; package body Nonlimited_Types is function Init (I : Integer) return Simple_Rec is begin return E : Simple_Rec do E.V := new Integer'(I); end return; end Init; procedure Set (E : Simple_Rec; I : Integer) is begin E.V.all := I; end Set; procedure Show (E : Simple_Rec; E_Name : String) is begin Put_Line (E_Name & ".V.all = " & Integer'Image (E.V.all)); end Show; end Nonlimited_Types;with Ada.Text_IO; use Ada.Text_IO; with Nonlimited_Types; use Nonlimited_Types; procedure Show_Wrong_Assignment_Equality is A, B : Simple_Rec := Init (0); procedure Show_Compare is begin if A = B then Put_Line ("A = B"); else Put_Line ("A /= B"); end if; end Show_Compare; begin Put_Line ("A := Init (0); A := Init (0);"); Show (A, "A"); Show (B, "B"); Show_Compare; Put_Line ("--------"); Put_Line ("Set (A, 2); Set (B, 3);"); Set (A, 2); Set (B, 3); Show (A, "A"); Show (B, "B"); Put_Line ("--------"); Put_Line ("B := A"); B := A; Show (A, "A"); Show (B, "B"); Show_Compare; Put_Line ("--------"); Put_Line ("Set (B, 7);"); Set (B, 7); Show (A, "A"); Show (B, "B"); Show_Compare; Put_Line ("--------"); end Show_Wrong_Assignment_Equality;
In this code, we declare the Simple_Rec
type in the
Nonlimited_Types
package and use it in the
Show_Wrong_Assignment_Equality
procedure. In principle, we're already
doing many things right here. For example, we're declaring the
Simple_Rec
type private, so that the component V
of access
type is encapsulated. Programmers that declare objects of this type cannot
simply mess up with the V
component. Instead, they have to call the
Init
function and the Set
procedure to initialize and change,
respectively, objects of the Simple_Rec
type. That being said, there are
two problems with this code, which we discuss next.
The first problem we can identify is that the first call to Show_Compare
shows that A
and B
are different, although both have the same
value in the V
component (A.V.all = 0
and B.V.all = 0
)
— this was set by the call to the Init
function. What's happening
here is that the A = B
expression is comparing the access values
(A.V = B.V
), while we might have been expecting it to compare the actual
integer values after dereferencing (A.V.all = B.V.all
). Therefore, the
predefined equality function of the Simple_Rec
type is useless and
dangerous for us, as it misleads us to expect something that it doesn't do.
After the assignment of A
to B
(B := A
), the information
that the application displays seems to be correct — both A.V.all
and B.V.all
have the same value of two. However, when assigning the
value seven to B
by calling Set (B, 7)
, we see that the value of
A.V.all
has also changed. What's happening here is that the previous
assignment (B := A
) has actually assigned access values
(B.V := A.V
), while we might have been expecting it to assign the
dereferenced values (B.V.all := A.V.all
). Therefore, we cannot simply
directly assign objects of Simple_Rec
type, as this operation changes
the internal structure of the type due to the presence of components of access
type.
For these reasons, forbidding these operations for the Simple_Rec
type
is the most appropriate software design decision. If we still need assignment
and equality operators, we can implement custom subprograms for the limited
type. We'll discuss this topic in the next sections.
In addition to the case when we have components of access types, limited types are useful for example when we want to avoid the situation in which the same information is copied to multiple objects of the same type.
In the Ada Reference Manual
Assignments
Assignments are forbidden when using objects of limited types. For example:
package Limited_Types is type Simple_Rec is limited private; type Integer_Access is access Integer; function Init (I : Integer) return Simple_Rec; private type Simple_Rec is limited record V : Integer_Access; end record; end Limited_Types;package body Limited_Types is function Init (I : Integer) return Simple_Rec is begin return E : Simple_Rec do E.V := new Integer'(I); end return; end Init; end Limited_Types;with Limited_Types; use Limited_Types; procedure Show_Limited_Assignment is A, B : Simple_Rec := Init (0); begin B := A; end Show_Limited_Assignment;
In this example, we declare the limited private type Simple_Rec
and two
objects of this type (A
and B
) in the
Show_Limited_Assignment
procedure. (We discuss more about limited
private types later).
As expected, we get a compilation error for the B := A
statement (in the
Show_Limited_Assignment
procedure). If we
need to copy two objects of limited type, we have to provide a custom procedure
to do that. For example, we can implement a Copy
procedure for the
Simple_Rec
type:
package Limited_Types is type Integer_Access is access Integer; type Simple_Rec is limited private; function Init (I : Integer) return Simple_Rec; procedure Copy (From : Simple_Rec; To : in out Simple_Rec); private type Simple_Rec is limited record V : Integer_Access; end record; end Limited_Types;package body Limited_Types is function Init (I : Integer) return Simple_Rec is begin return E : Simple_Rec do E.V := new Integer'(I); end return; end Init; procedure Copy (From : Simple_Rec; To : in out Simple_Rec) is begin -- Copying record components To.V.all := From.V.all; end Copy; end Limited_Types;with Limited_Types; use Limited_Types; procedure Show_Limited_Assignment is A, B : Simple_Rec := Init (0); begin Copy (From => A, To => B); end Show_Limited_Assignment;
The Copy
procedure from this example copies the dereferenced values of
From
to To
, which matches our expectation for the
Simple_Rec
. Note that we could have also implemented a
Shallow_Copy
procedure to copy the actual access values (i.e.
To.V := From.V
). However, having this kind of procedure can be dangerous
in many case, so this design decision must be made carefully. In any case,
using limited types ensures that only the assignment subprograms that are
explicitly declared in the package specification are available.
Equality
Limited types don't have a predefined equality operator. For example:
package Limited_Types is type Integer_Access is access Integer; type Simple_Rec is limited private; function Init (I : Integer) return Simple_Rec; private type Simple_Rec is limited record V : Integer_Access; end record; end Limited_Types;package body Limited_Types is function Init (I : Integer) return Simple_Rec is begin return E : Simple_Rec do E.V := new Integer'(I); end return; end Init; end Limited_Types;with Ada.Text_IO; use Ada.Text_IO; with Limited_Types; use Limited_Types; procedure Show_Limited_Equality is A : Simple_Rec := Init (5); B : Simple_Rec := Init (6); begin if A = B then Put_Line ("A = B"); else Put_Line ("A /= B"); end if; end Show_Limited_Equality;
As expected, the comparison A = B
triggers a compilation error because
no predefined =
operator is available for the Simple_Rec
type.
If we want to be able to compare objects of this type, we have to implement
the =
operator ourselves. For example, we can do that for the
Simple_Rec
type:
package Limited_Types is type Integer_Access is access Integer; type Simple_Rec is limited private; function Init (I : Integer) return Simple_Rec; function "=" (Left, Right : Simple_Rec) return Boolean; private type Simple_Rec is limited record V : Integer_Access; end record; end Limited_Types;package body Limited_Types is function Init (I : Integer) return Simple_Rec is begin return E : Simple_Rec do E.V := new Integer'(I); end return; end Init; function "=" (Left, Right : Simple_Rec) return Boolean is begin -- Comparing record components return Left.V.all = Right.V.all; end "="; end Limited_Types;with Ada.Text_IO; use Ada.Text_IO; with Limited_Types; use Limited_Types; procedure Show_Limited_Equality is A : Simple_Rec := Init (5); B : Simple_Rec := Init (6); begin if A = B then Put_Line ("A = B"); else Put_Line ("A /= B"); end if; end Show_Limited_Equality;
Here, the =
operator compares the dereferenced values of Left.V
and Right.V
, which matches our expectation for the Simple_Rec
type. Declaring types as limited ensures that we don't have unreasonable
equality comparisons, and allows us to create reasonable replacements when
required.
In other languages
In C++, you can overload the assignment operator. For example:
class Simple_Rec
{
public:
// Overloaded assignment
Simple_Rec& operator= (const Simple_Rec& obj);
private:
int *V;
};
In Ada, however, we can only define the equality operator (=
).
Defining the assignment operator (:=
) is not possible. The following
code triggers a compilation error as expected:
package Limited_Types is
type Integer_Access is access Integer;
type Simple_Rec is limited private;
procedure ":=" (To : in out Simple_Rec
From : Simple_Rec);
-- ...
end Limited_Types;
Limited private types
As we've seen in code examples from the previous section, we can apply
information hiding to limited types. In other words,
we can declare a type as limited private
instead of just limited
.
For example:
package Simple_Recs is type Rec is limited private; private type Rec is limited record I : Integer; end record; end Simple_Recs;
In this case, in addition to the fact that assignments are forbidden for
objects of this type (because Rec
is limited), we cannot access the
record components.
Note that in this example, both partial and full views of the Rec
record are of limited type. In the next sections, we discuss how the partial
and full views can have non-matching declarations.
In the Ada Reference Manual
Non-Record Limited Types
In principle, only record types can be declared limited, so we cannot use scalar or array types. For example, the following declarations won't compile:
package Non_Record_Limited_Error is type Limited_Enumeration is limited (Off, On); type Limited_Integer is new limited Integer; type Integer_Array is array (Positive range <>) of Integer; type Rec is new limited Integer_Array (1 .. 2); end Non_Record_Limited_Error;
However, we've mentioned in a previous chapter that private types don't have to be record types necessarily. In this sense, limited private types makes it possible for us to use types other than record types in the full view and still benefit from the restrictions of limited types. For example:
package Simple_Recs is type Limited_Enumeration is limited private; type Limited_Integer is limited private; type Limited_Integer_Array_2 is limited private; private type Limited_Enumeration is (Off, On); type Limited_Integer is new Integer; type Integer_Array is array (Positive range <>) of Integer; type Limited_Integer_Array_2 is new Integer_Array (1 .. 2); end Simple_Recs;
Here, Limited_Enumeration
, Limited_Integer
, and
Limited_Integer_Array_2
are limited private types that encapsulate an
enumeration type, an integer type, and a constrained array type, respectively.
Partial and full view of limited types
In the previous example, both partial and full views of the Rec
type
were limited. We may actually declare a type as limited private
(in the
public part of a package), while its full view is nonlimited. For example:
package Simple_Recs is type Rec is limited private; -- Partial view of Rec is limited private type Rec is record -- Full view of Rec is nonlimited I : Integer; end record; end Simple_Recs;
In this case, only the partial view of Rec
is limited, while its full
view is nonlimited. When deriving from Rec
, the view of the derived
type is the same as for the parent type:
package Simple_Recs.Child is type Rec_Derived is new Rec; -- As for its parent, the -- partial view of Rec_Derived -- is limited, but the full view -- is nonlimited. end Simple_Recs.Child;
Clients must nevertheless comply with their partial view, and treat the type as
if it is in fact limited. In other words, if you use the Rec
type in a
subprogram or package outside of the Simple_Recs
package (or its child
packages), the type is limited from that perspective:
with Simple_Recs; use Simple_Recs; procedure Use_Rec_In_Subprogram is R1, R2 : Rec; begin R1.I := 1; R2 := R1; end Use_Rec_In_Subprogram;
Here, compilation fails because the type Rec
is limited from the
procedure's perspective.
Limitations
Note that the opposite — declaring a type as private
and its full
full view as limited private
— is not possible. For example:
package Simple_Recs is type Rec is private; private type Rec is limited record I : Integer; end record; end Simple_Recs;
As expected, we get a compilation error in this case. The issue is that the partial view cannot be allowed to mislead the client about what's possible. In this case, if the partial view allows assignment, then the full view must actually provide assignment. But the partial view can restrict what is actually possible, so a limited partial view need not be completed in the full view as a limited type.
In addition, tagged limited private types cannot have a nonlimited full view. For example:
package Simple_Recs is type Rec is tagged limited private; private type Rec is tagged record I : Integer; end record; end Simple_Recs;
Here, compilation fails because the type Rec
is nonlimited in its full
view.
Limited and nonlimited in full view
Declaring the full view of a type as limited or nonlimited has implications in the way we can use objects of this type in the package body. For example:
package Simple_Recs is type Rec_Limited_Full is limited private; type Rec_Nonlimited_Full is limited private; procedure Copy (From : Rec_Limited_Full; To : in out Rec_Limited_Full); procedure Copy (From : Rec_Nonlimited_Full; To : in out Rec_Nonlimited_Full); private type Rec_Limited_Full is limited record I : Integer; end record; type Rec_Nonlimited_Full is record I : Integer; end record; end Simple_Recs;package body Simple_Recs is procedure Copy (From : Rec_Limited_Full; To : in out Rec_Limited_Full) is begin To := From; -- ERROR: assignment is forbidden because -- Rec_Limited_Full is limited in -- its full view. end Copy; procedure Copy (From : Rec_Nonlimited_Full; To : in out Rec_Nonlimited_Full) is begin To := From; -- OK: assignment is allowed because -- Rec_Nonlimited_Full is -- nonlimited in its full view. end Copy; end Simple_Recs;
Here, both Rec_Limited_Full
and Rec_Nonlimited_Full
are declared
as private limited
. However, Rec_Limited_Full
type is limited in
its full view, while Rec_Nonlimited_Full
is nonlimited. As expected,
the compiler complains about the To := From
assignment in the
Copy
procedure for the Rec_Limited_Full
type because its full
view is limited (so no assignment is possible). Of course, in the case of the
objects of Rec_Nonlimited_Full
type, this assignment is perfectly fine.
Limited private component
Another example mentioned by the Ada Reference Manual (7.3.1, 5/1) is about an array type whose component type is limited private, but nonlimited in its full view. Let's see a complete code example for that:
package Limited_Nonlimited_Arrays is type Limited_Private is limited private; function Init return Limited_Private; -- The array type Limited_Private_Array -- is limited because the type of its -- component is limited. type Limited_Private_Array is array (Positive range <>) of Limited_Private; private type Limited_Private is record A : Integer; end record; -- Limited_Private_Array type is -- nonlimited at this point because -- its component is nonlimited. -- -- The assignments below are OK: A1 : Limited_Private_Array (1 .. 5); A2 : Limited_Private_Array := A1; end Limited_Nonlimited_Arrays;package body Limited_Nonlimited_Arrays is function Init return Limited_Private is ((A => 1)); end Limited_Nonlimited_Arrays;with Limited_Nonlimited_Arrays; use Limited_Nonlimited_Arrays; procedure Show_Limited_Nonlimited_Array is A3 : Limited_Private_Array (1 .. 2) := (others => Init); A4 : Limited_Private_Array (1 .. 2); begin -- ERROR: this assignment is illegal because -- Limited_Private_Array is limited, as -- its component is limited at this point. A4 := A3; end Show_Limited_Nonlimited_Array;
As we can see in this example, the limitedness of the array type
Limited_Private_Array
depends on the limitedness of its component type
Limited_Private
. In the private part of Limited_Nonlimited_Arrays
package, where Limited_Private
is nonlimited, the array type
Limited_Private_Array
becomes nonlimited as well. In contrast, in the
Show_Limited_Nonlimited_Array
, the array type is limited because its
component is limited in that scope.
In the Ada Reference Manual
Tagged limited private types
For tagged private types, the partial and full views must match: if a tagged type is limited in the partial view, it must be limited in the full view. For example:
package Simple_Recs is type Rec is tagged limited private; private type Rec is tagged limited record I : Integer; end record; end Simple_Recs;
Here, the tagged Rec
type is limited both in its partial and full views.
Any mismatch in one of the views triggers a compilation error. (As an
exercise, you may remove any of the limited
keywords from the code
example and try to compile it.)
For further reading...
This rule is for the sake of dynamic dispatching and classwide types. The compiler must not allow any of the types in a derivation class — the set of types related by inheritance — to be different regarding assignment and equality (and thus inequality). That's necessary because we are meant to be able to manipulate objects of any type in the entire set of types via the partial view presented by the root type, without knowing which specific tagged type is involved.
Explicitly limited types
Under certain conditions, limited types can be called explicitly limited
— note that using the limited
keyword in a part of the declaration
doesn't necessary ensure this, as we'll see later.
Let's start with an example of an explicitly limited type:
package Simple_Recs is type Rec is limited record I : Integer; end record; end Simple_Recs;
The Rec
type is also explicitly limited when it's declared limited in
the private type's completion (in the package's private part):
package Simple_Recs is type Rec is limited private; private type Rec is limited record I : Integer; end record; end Simple_Recs;
In this case, Rec
is limited both in the partial and in the full view,
so it's considered explicitly limited.
However, as we've learned before,
we may actually declare a type as limited private
in the
public part of a package, while its full view is nonlimited. In this case, the
limited type is not considered explicitly limited anymore.
For example, if we make the full view of the Rec
nonlimited (by
removing the limited
keyword in the private part), then the Rec
type isn't explicitly limited anymore:
package Simple_Recs is type Rec is limited private; private type Rec is record I : Integer; end record; end Simple_Recs;
Now, even though the Rec
type was declared as limited private, the full
view indicates that it's actually a nonlimited type, so it isn't explicitly
limited.
Note that tagged limited private types are always explicitly limited types — because, as we've learned before, they cannot have a nonlimited type declaration in its full view.
In the Ada Reference Manual
Subtypes of Limited Types
We can declare subtypes of limited types. For example:
package Simple_Recs is type Limited_Integer_Array (L : Positive) is limited private; subtype Limited_Integer_Array_2 is Limited_Integer_Array (2); private type Integer_Array is array (Positive range <>) of Integer; type Limited_Integer_Array (L : Positive) is limited record Arr : Integer_Array (1 .. L); end record; end Simple_Recs;
Here, Limited_Integer_Array_2
is a subtype of the
Limited_Integer_Array
type. Since Limited_Integer_Array
is a
limited type, the Limited_Integer_Array_2
subtype is limited as well.
A subtype just introduces a name for some constraints on an existing type. As
such, a subtype doesn't change the limitedness of the constrained type.
We can test this in a small application:
with Simple_Recs; use Simple_Recs; procedure Test_Limitedness is Dummy_1, Dummy_2 : Limited_Integer_Array_2; begin Dummy_2 := Dummy_1; end Test_Limitedness;
As expected, compilations fails because Limited_Integer_Array_2
is a
limited (sub)type.
Deriving from limited types
In this section, we discuss the implications of deriving from limited types. As usual, let's start with a simple example:
package Simple_Recs is type Rec is limited null record; type Rec_Derived is new Rec; end Simple_Recs;
In this example, the Rec_Derived
type is derived from the Rec
type. Note that the Rec_Derived
type is limited because its ancestor is
limited, even though the limited
keyword doesn't show up in the
declaration of the Rec_Derived
type. Note that we could have actually
used the limited
keyword here:
type Rec_Derived is limited new Rec;
Therefore, we cannot use the assignment operator for objects of
Rec_Derived
type:
with Simple_Recs; use Simple_Recs; procedure Test_Limitedness is Dummy_1, Dummy_2 : Rec_Derived; begin Dummy_2 := Dummy_1; end Test_Limitedness;
Note that we cannot derive a limited type from a nonlimited ancestor:
package Simple_Recs is type Rec is null record; type Rec_Derived is limited new Rec; end Simple_Recs;
As expected, the compiler indicates that the ancestor Rec
should be of
limited type.
In fact, all types in a derivation class are the same — either limited or not. (That is especially important with dynamic dispatching via tagged types. We discuss this topic in another chapter.)
In the Ada Reference Manual
Deriving from limited private types
Of course, we can also derive from limited private types. However, there are more rules in this case than the ones we've seen so far. Let's start with an example:
package Simple_Recs is type Rec is limited private; private type Rec is limited null record; end Simple_Recs;package Simple_Recs.Ext is type Rec_Derived is new Rec; -- OR: -- -- type Rec_Derived is -- limited new Rec; end Simple_Recs.Ext;with Simple_Recs.Ext; use Simple_Recs.Ext; procedure Test_Limitedness is Dummy_1, Dummy_2 : Rec_Derived; begin Dummy_2 := Dummy_1; end Test_Limitedness;
Here, Rec_Derived
is a limited type derived from the (limited private)
Rec
type. We can verify that Rec_Derived
type is limited
because the compilation of the Test_Limitedness
procedure fails.
Deriving from non-explicitly limited private types
Up to this point, we have discussed explicitly limited types. Now, let's see how derivation works with non-explicitly limited types.
Any type derived from a limited type is always limited, even if the full view
of its ancestor is nonlimited. For example, let's modify the full view of
Rec
and make it nonlimited (i.e. make it not explicitly limited):
package Simple_Recs is type Rec is limited private; private type Rec is null record; end Simple_Recs;
Here, Rec_Derived
is a limited type because the partial view of
Rec
is limited. The fact that the full view of Rec
is nonlimited
doesn't affect the Rec_Derived
type — as we can verify with the
compilation error in the Test_Limitedness
procedure.
Note, however, that a derived type becomes nonlimited in the
private part or the body of a child package if it isn't explicitly limited.
In this sense, the derived type inherits the nonlimitedness of the parent's
full view. For example,
because we're declaring Rec_Derived
as is new Rec
in the child
package (Simple_Recs.Ext
), we're saying that Rec_Derived
is
limited outside this package, but nonlimited in the private part and body of
the Simple_Recs.Ext
package. We can verify this by copying the code from
the Test_Limitedness
procedure to a new procedure in the body of the
Simple_Recs.Ext
package:
package Simple_Recs.Ext with Elaborate_Body is -- Rec_Derived is derived from Rec, which is a -- limited private type that is nonlimited in -- its full view. -- -- Rec_Derived isn't explicitly limited. -- Therefore, it's nonlimited in the private -- part of Simple_Recs.Ext and its package -- body. -- type Rec_Derived is new Rec; end Simple_Recs.Ext;package body Simple_Recs.Ext is procedure Test_Child_Limitedness is Dummy_1, Dummy_2 : Rec_Derived; begin -- Here, Rec_Derived is a nonlimited -- type because Rec is nonlimited in -- its full view. Dummy_2 := Dummy_1; end Test_Child_Limitedness; end Simple_Recs.Ext;-- We copied the code to the -- Test_Child_Limitedness procedure (in the -- body of the Simple_Recs.Ext package) and -- commented it out here. -- -- You may uncomment the code to verify -- that Rec_Derived is limited in this -- procedure. -- -- with Simple_Recs.Ext; use Simple_Recs.Ext; procedure Test_Limitedness is -- Dummy_1, Dummy_2 : Rec_Derived; begin -- Dummy_2 := Dummy_1; null; end Test_Limitedness;
In the Test_Child_Limitedness
procedure of the Simple_Recs.Ext
package, we can use the Rec_Derived
as a nonlimited type because its
ancestor Rec
is nonlimited in its full view. (
As we've learned before, if a
limited type is nonlimited in its full view, we can copy objects of this type
in the private part of the package specification or in the package body.)
Outside of the package, both Rec
and Rec_Derived
types are
limited types. Therefore, if we uncomment the code in the
Test_Limitedness
procedure, compilation fails there (because
Rec_Derived
is viewed as descending from a limited type).
Deriving from tagged limited private types
The rules for deriving from tagged limited private types are slightly different than the rules we've seen so far. This is because tagged limited types are always explicitly limited types.
Let's look at an example:
package Simple_Recs is type Tagged_Rec is tagged limited private; private type Tagged_Rec is tagged limited null record; end Simple_Recs;package Simple_Recs.Ext is type Rec_Derived is new Tagged_Rec with private; private type Rec_Derived is new Tagged_Rec with null record; end Simple_Recs.Ext;with Simple_Recs.Ext; use Simple_Recs.Ext; procedure Test_Limitedness is Dummy_1, Dummy_2 : Rec_Derived; begin Dummy_2 := Dummy_1; end Test_Limitedness;
In this example, Rec_Derived
is a tagged limited type derived from the
Tagged_Rec
type. (Again, we can verify the limitedness of the
Rec_Derived
type with the Test_Limitedness
procedure.)
As explained previously, the derived type (Rec_Derived
) is a limited
type, even though the limited
keyword doesn't appear in its
declaration. We could, of course, include the limited
keyword in the
declaration of Rec_Derived
:
package Simple_Recs.Ext is type Rec_Derived is limited new Tagged_Rec with private; private type Rec_Derived is limited new Tagged_Rec with null record; end Simple_Recs.Ext;
(Obviously, if we include the limited
keyword in the partial view of
the derived type, we must include it in its full view as well.)
Deriving from limited interfaces
The rules for limited interfaces are different from the ones for limited tagged types. In contrast to the rule we've seen in the previous section, a type that is derived from a limited type isn't automatically limited. In other words, it does not inherit the limitedness from the interface. For example:
package Simple_Recs is type Limited_IF is limited interface; end Simple_Recs;package Simple_Recs.Ext is type Rec_Derived is new Limited_IF with private; private type Rec_Derived is new Limited_IF with null record; end Simple_Recs.Ext;with Simple_Recs.Ext; use Simple_Recs.Ext; procedure Test_Limitedness is Dummy_1, Dummy_2 : Rec_Derived; begin Dummy_2 := Dummy_1; end Test_Limitedness;
Here, Rec_Derived
is derived from the limited Limited_IF
interface. As we can see, the Test_Limitedness
compiles fine because
Rec_Derived
is nonlimited.
Of course, if we want Rec_Derived
to be limited, we can make this
explicit in the type declaration:
package Simple_Recs.Ext is type Rec_Derived is limited new Limited_IF with private; private type Rec_Derived is limited new Limited_IF with null record; end Simple_Recs.Ext;with Simple_Recs.Ext; use Simple_Recs.Ext; procedure Test_Limitedness is Dummy_1, Dummy_2 : Rec_Derived; begin Dummy_2 := Dummy_1; end Test_Limitedness;
Now, compilation of Test_Limitedness
fails because Rec_Derived
is
explicitly limited.
Immutably Limited Types
According to the Annotated Ada Reference Manual (7.5, 8.b/3), "an immutably limited type is a type that cannot become nonlimited subsequently in a private part or in a child unit." In fact, while we were talking about partial and full view of limited types, we've seen that limited private types can become nonlimited in their full view. Such limited types are not immutably limited.
The Annotated Ada Reference Manual also says that "if a view of the type makes it immutably limited, then no copying (assignment) operations are ever available for objects of the type. This allows other properties; for instance, it is safe for such objects to have access discriminants that have defaults or designate other limited objects." We'll see examples of this later on.
Immutably limited types include:
tagged limited types (i.e. with the keywords
tagged limited
);limited private type that have at least one access discriminant with a default expression;
task types, protected types, and synchronized interfaces;
any types derived from immutably limited types.
Let's look at a code example that shows instances of immutably limited types:
package Show_Immutably_Limited_Types is -- -- Explicitly limited type -- type Explicitly_Limited_Rec is limited record A : Integer; end record; -- -- Tagged limited type -- type Limited_Tagged_Rec is tagged limited record A : Integer; end record; -- -- Tagged limited private type -- type Limited_Tagged_Private is tagged limited private; -- -- Limited private type with an access -- discriminant that has a default -- expression -- type Limited_Rec_Access_D (AI : access Integer := new Integer) is limited private; -- -- Task type -- task type TT is entry Start; entry Stop; end TT; -- -- Protected type -- protected type PT is function Value return Integer; private A : Integer; end PT; -- -- Synchronized interface -- type SI is synchronized interface; -- -- A type derived from an immutably -- limited type -- type Derived_Immutable is new Explicitly_Limited_Rec; private type Limited_Tagged_Private is tagged limited record A : Integer; end record; type Limited_Rec_Access_D (AI : access Integer := new Integer) is limited record A : Integer; end record; end Show_Immutably_Limited_Types;package body Show_Immutably_Limited_Types is task body TT is begin accept Start; accept Stop; end TT; protected body PT is function Value return Integer is (PT.A); end PT; end Show_Immutably_Limited_Types;
In the Show_Immutably_Limited_Types
package above, we see multiple
instances of immutably limited types. (The comments in the source code indicate
each type.)
In the Ada Reference Manual
Non immutably limited types
Not every limited type is immutably limited. We already mentioned untagged private limited types, which can become nonlimited in their full view. In addition, we have nonsynchronized limited interface types. As mentioned earlier in this chapter, a type derived from a nonsynchronized limited interface, can be nonlimited, so it's not immutably limited.
In the Ada Reference Manual
Limited Types with Discriminants
In this section, we look into the implications of using discriminants with limited types. Actually, most of the topics mentioned here have already been covered in different sections of previous chapters, as well as in this chapter. Therefore, this section is in most parts just a review of what we've already discussed.
Let's start with a simple example:
package Simple_Recs is type Rec (L : Positive) is limited null record; end Simple_Recs;with Simple_Recs; use Simple_Recs; procedure Test_Limitedness is Dummy_1 : Rec (2); Dummy_2 : Rec (3); begin Dummy_2 := Dummy_1; -- ^^^^^^^^^^^^^^ -- ERRORS: -- 1. Cannot assign objects of -- limited types. -- 2. Cannot assign objects with -- different discriminants. end Test_Limitedness;
In this example, we see the declaration of the limited type Rec
, which
has the discriminant L
. For objects of type Rec
, we not only have
the typical restrictions that
equality and assignment aren't available,
but we also have the restriction that we won't be able to assign objects
with different discriminants.
In the Ada Reference Manual
Default Expressions
On the other hand, there are restrictions that apply to nonlimited types with discriminants, but not to limited types with discriminants. This concerns mostly default expressions, which are generally allowed for discriminants of limited types.
Discriminants of tagged limited types
As we've discussed previously, we can use default expressions for discriminants of tagged limited types. Let's see an example:
package Recs is type LTT (L : Positive := 1; M : Positive := 2) is tagged limited null record; end Recs;
Obviously, the same applies to tagged limited private types:
package Recs is type LTT (L : Positive := 1; M : Positive := 2) is tagged limited private; private type LTT (L : Positive := 1; M : Positive := 2) is tagged limited null record; end Recs;
In the case of tagged, nonlimited types, using default expressions in this context isn't allowed.
Access discriminant
Similarly, when using limited types, we can specify default expressions for access discriminants:
package Custom_Recs is -- Specifying a default expression for -- an access discriminant: type Rec (IA : access Integer := new Integer'(0)) is limited record I : Integer := IA.all; end record; end Custom_Recs;
In fact, as we've discussed before, this isn't possible for nonlimited types.
Note, however, that we can only assign a default expression to an access discriminant of an immutably limited type.
Discriminants of nontagged limited types
In addition to tagged limited types, we can use default expressions for discriminants of nontagged limited types. Let's see an example:
package Recs is type LTT (L : Positive := 1; M : Positive := 2) is limited null record; end Recs;
Obviously, the same applies to limited private types:
package Recs is type LTT (L : Positive := 1; M : Positive := 2) is limited private; private type LTT (L : Positive := 1; M : Positive := 2) is limited null record; end Recs;
Note that using default expressions for discriminants of nonlimited, nontagged types is OK as well.
Mutable subtypes and Limitedness
As we've mentioned before, an unconstrained discriminated subtype with defaults is called a mutable subtype. An important feature of mutable subtypes is that it allows changing the discriminants of an object, e.g. via assignments. However, as we know, we cannot assign to objects of limited types. Therefore, in essence, a type should be nonlimited to be considered a mutable subtype.
Let's look at a code example:
package Recs is type LTT (L : Positive := 1; M : Positive := 2) is limited null record; function Init (L : Positive; M : Positive) return LTT is ((L => L, M => M)); procedure Copy (From : LTT; To : in out LTT); end Recs;package body Recs is procedure Copy (From : LTT; To : in out LTT) is begin To := Init (L => From.L, M => From.M); -- ERROR: cannot assign to object of -- limited type To.L := From.L; To.M := From.M; -- ERROR: cannot change discriminants end Copy; end Recs;with Recs; use Recs; procedure Show is A : LTT; B : LTT := Init (10, 12); begin Copy (From => B, To => A); end Show;
As we can see in the Copy
procedure, it's not possible to properly
assign to the target object. Using Init
is forbidden because the
assignment is not initializing the target object — as we're not declaring
To
at this point. Also, changing the individual discriminants is
forbidden as well. Therefore, we don't have any means to change the
discriminants of the target object. (In contrast, if LTT
was a
nonlimited type, we would be able to implement Copy
by using the call to
the Init
function.)
Limited private type with unknown discriminants
We can declare limited private types with unknown discriminants. Let's see an example:
package Limited_Private_Unknown_Discriminants is type Rec (<>) is limited private; private type Rec is limited record I : Integer; end record; end Limited_Private_Unknown_Discriminants;
In this example, we declare type Rec
, which has unknown discriminants.
As we mentioned earlier, when we use a private type with unknown discriminants, we gain extra control over its initialization. In addition, if we declare those types as limited, we gain even more control. In fact, this is what the Annotated Ada Reference Manual (3.7, 26.b/2) says:
"A subtype with unknown discriminants is indefinite, and hence an object of such a subtype needs explicit initialization. A limited private type with unknown discriminants is 'extremely' limited; objects of such a type can be initialized only by subprograms (either procedures with a parameter of the type, or a function returning the type) declared in the package. Subprograms declared elsewhere can operate on and even return the type, but they can only initialize the object by calling (ultimately) a subprogram in the package declaring the type. Such a type is useful for keeping complete control over object creation within the package declaring the type."
Let's reuse a code example from the previous section on unknown discriminants and use limited types:
package Limited_Private_Unknown_Discriminants is type Rec (<>) is limited private; function Init return Rec; private type Rec is limited record I : Integer; end record; function Init return Rec is ((I => 0)); end Limited_Private_Unknown_Discriminants;with Limited_Private_Unknown_Discriminants; use Limited_Private_Unknown_Discriminants; procedure Show_Constructor_Function is R : Rec := Init; begin null; end Show_Constructor_Function;
A function such as Init
is called a
constructor function for limited types.
We discuss this topic in more detail later on.
Record components of limited type
In this section, we discuss the implications of using components of limited type. Let's start by declaring a record component of limited type:
package Simple_Recs is type Int_Rec is limited record V : Integer; end record; type Rec is limited record IR : Int_Rec; end record; end Simple_Recs;
As soon as we declare a record component of some limited type, the whole record
is limited. In this example, the Rec
record is limited due to the
presence of the IR
component of limited type.
Also, if we change the declaration of the Rec
record from the previous
example and remove the limited
keyword, the type itself remains
implicitly limited. We can see that when trying to assign to objects of
Rec
type in the Show_Implicitly_Limited
procedure:
package Simple_Recs is type Int_Rec is limited record V : Integer; end record; type Rec is record IR : Int_Rec; end record; end Simple_Recs;with Simple_Recs; use Simple_Recs; procedure Show_Implicitly_Limited is A, B : Rec; begin B := A; end Show_Implicitly_Limited;
Here, the compiler indicates that the assignment is forbidden because the
Rec
type has a component of limited type. The rationale for this rule is
that an object of a limited type doesn't allow assignment or equality,
including the case in which that object is a component of some enclosing
composite object. If we allowed the enclosing object to be copied or tested for
equality, we'd be doing it for all the components, too.
In the Ada Reference Manual
Limited types and aggregates
Note
This section was originally written by Robert A. Duff and published as Gem #1: Limited Types in Ada 2005 and Gem #2.
In this section, we focus on using aggregates to initialize limited types.
Historically
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:
with Ada.Strings.Unbounded; use Ada.Strings.Unbounded; package Persons is type Limited_Person; type Limited_Person_Access is access all Limited_Person; type Limited_Person is limited record Name : Unbounded_String; Age : Natural; end record; end Persons;with Ada.Strings.Unbounded; use Ada.Strings.Unbounded; with Persons; use Persons; procedure Show_Non_Aggregate_Init is X : Limited_Person; begin X.Name := To_Unbounded_String ("John Doe"); X.Age := 25; end Show_Non_Aggregate_Init;
which has the maintenance problem the full coverage rules are supposed to prevent. Or, make the type nonlimited, and gain the benefits of aggregates, but lose the ability to prevent copies.
Full coverage rules for limited types
Previously, we discussed full coverage rules for aggregates. They also apply to limited types.
Historically
The full coverage rules have been aiding maintenance since Ada 83. However, prior to Ada 2005, we couldn't use them for limited types.
Suppose we have the following limited type:
with Ada.Strings.Unbounded; use Ada.Strings.Unbounded; package Persons is type Limited_Person; type Limited_Person_Access is access all Limited_Person; type Limited_Person is limited record Self : Limited_Person_Access := Limited_Person'Unchecked_Access; Name : Unbounded_String; Age : Natural; Shoe_Size : Positive; end record; end Persons;
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.
We can initialize objects of limited type with an aggregate. Here, we can say:
with Ada.Strings.Unbounded; use Ada.Strings.Unbounded; with Persons; use Persons; procedure Show_Aggregate_Box_Init is X : aliased Limited_Person := (Self => <>, Name => To_Unbounded_String ("John Doe"), Age => 25, Shoe_Size => 10); begin null; end Show_Aggregate_Box_Init;
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
.
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.
Historically
Since Ada 2005, an aggregate is allowed to be limited; we can say:
with Ada.Strings.Unbounded; use Ada.Strings.Unbounded; with Persons; use Persons; procedure Show_Aggregate_Init is X : aliased Limited_Person := (Self => null, -- Wrong! Name => To_Unbounded_String ("John Doe"), Age => 25, Shoe_Size => 10); begin X.Self := X'Unchecked_Access; end Show_Aggregate_Init;
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".
Therefore, we can replace Self => null
by Self => <>
.
Important
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:
procedure Show_String_Box_Init is Uninitialized_Const_Str : constant String := (1 .. 10 => <>); begin null; end Show_String_Box_Init;
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:
procedure Show_Dangerous_String is Uninitialized_String_Var : String (1 .. 10); -- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -- no initialization Uninitialized_Const_Str : constant String := Uninitialized_String_Var; begin null; end Show_Dangerous_String;
As always, one must be careful about uninitialized scalar objects.
Constructor functions for limited types
Note
This section was originally written by Robert A. Duff and published as Gem #3.
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.
Historically
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.
Let's see an example:
with Ada.Strings.Unbounded; use Ada.Strings.Unbounded; package P is task type Some_Task_Type; protected type Some_Protected_Type is -- dummy type end Some_Protected_Type; type T (<>) is limited private; function Make_T (Name : String) return T; -- ^^^^^^ -- constructor function private type T is limited record Name : Unbounded_String; My_Task : Some_Task_Type; My_Prot : Some_Protected_Type; end record; end P;package body P is task body Some_Task_Type is begin null; end Some_Task_Type; protected body Some_Protected_Type is end Some_Protected_Type; function Make_T (Name : String) return T is begin return (Name => To_Unbounded_String (Name), others => <>); end Make_T; end P;
Given the above, clients can say:
with P; use P; procedure Show_Constructor_Function is My_T : T := Make_T (Name => "Bartholomew Cubbins"); begin null; end Show_Constructor_Function;
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:
with P; use P; procedure Show_Rumplestiltskin_Constructor is function Make_Rumplestiltskin return T is begin return Make_T (Name => "Rumplestiltskin"); end Make_Rumplestiltskin; Rumplestiltskin_Is_My_Name : constant T := Make_Rumplestiltskin; begin null; end Show_Rumplestiltskin_Constructor;
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
.
Historically
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.
As we discussed before,
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 nonlimited types should be just the same, except for the essential difference: you can't copy limited objects (and there's no language-defined equality operator). 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.
Historically
Prior to Ada 2005, constructors only worked for nonlimited 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:
with Ada.Containers.Ordered_Sets; package Aux is generic with package OS is new Ada.Containers.Ordered_Sets (<>); function Gen_Singleton_Set (Element : OS.Element_Type) return OS.Set; end Aux;package body Aux is function Gen_Singleton_Set (Element : OS.Element_Type) return OS.Set is begin return S : OS.Set := OS.Empty_Set do S.Insert (Element); end return; end Gen_Singleton_Set; end Aux;
Since Ada 2005, we can say:
with Ada.Containers.Ordered_Sets; with Aux; procedure Show_Set_Decl is package Integer_Sets is new Ada.Containers.Ordered_Sets (Element_Type => Integer); use Integer_Sets; function Singleton_Set is new Aux.Gen_Singleton_Set (OS => Integer_Sets); This_Set : Set := Empty_Set; That_Set : Set := Singleton_Set (Element => 42); begin null; end Show_Set_Decl;
whether or not Set
is limited. This_Set : Set := Empty_Set;
seems clearer than:
with Ada.Containers.Ordered_Sets; procedure Show_Set_Decl is package Integer_Sets is new Ada.Containers.Ordered_Sets (Element_Type => Integer); use Integer_Sets; This_Set : Set; begin null; end Show_Set_Decl;
which might mean "default-initialize to the empty set" or might mean "leave it uninitialized, and we'll initialize it in later".
Return objects
Extended return statements for limited types
Note
This section was originally written by Robert A. Duff and published as Gem #10: Limited Types in Ada 2005.
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:
package Task_Construct_Error is task type Task_Type (Discriminant : Integer); function Make_Task (Val : Integer) return Task_Type; end Task_Construct_Error;package body Task_Construct_Error is task body Task_Type is begin null; end Task_Type; function Make_Task (Val : Integer) return Task_Type is Result : Task_Type (Discriminant => Val * 3); begin -- some statements... return Result; -- Illegal! end Make_Task; end Task_Construct_Error;
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:
package Task_Construct is task type Task_Type (Discriminant : Integer); function Make_Task (Val : Integer) return Task_Type; end Task_Construct;package body Task_Construct is task body Task_Type is begin null; end Task_Type; function Make_Task (Val : Integer) return Task_Type is begin return Result : Task_Type (Discriminant => Val * 3) do -- some statements... null; end return; end Make_Task; end Task_Construct;
If we call it like this:
with Task_Construct; use Task_Construct; procedure Show_Task_Construct is My_Task : Task_Type := Make_Task (Val => 42); begin null; end Show_Task_Construct;
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.
Initialization and function return
As mentioned in the previous section, the object of limited type returned by the initialization function is built in place. In other words, the return object is built in the object that is the target of the assignment statement.
For example, we can see this when looking at the address of the object
returned by the Init
function, which we call to initialize the limited
type Simple_Rec
:
package Limited_Types is type Integer_Access is access Integer; type Simple_Rec is limited private; function Init (I : Integer) return Simple_Rec; private type Simple_Rec is limited record V : Integer_Access; end record; end Limited_Types;with Ada.Text_IO; use Ada.Text_IO; with System; with System.Address_Image; package body Limited_Types is function Init (I : Integer) return Simple_Rec is begin return E : Simple_Rec do E.V := new Integer'(I); Put_Line ("E'Address (Init): " & System.Address_Image (E'Address)); end return; end Init; end Limited_Types;with Ada.Text_IO; use Ada.Text_IO; with System; with System.Address_Image; with Limited_Types; use Limited_Types; procedure Show_Limited_Init is begin declare A : Simple_Rec := Init (0); begin Put_Line ("A'Address (local): " & System.Address_Image (A'Address)); end; Put_Line ("----"); declare B : Simple_Rec := Init (0); begin Put_Line ("B'Address (local): " & System.Address_Image (B'Address)); end; end Show_Limited_Init;
When running this code example and comparing the address of the object E
in the Init
function and the object that is being initialized in the
Show_Limited_Init
procedure, we see that the return object E
(of
the Init
function) and the local object in the Show_Limited_Init
procedure are the same object.
Important
When we use nonlimited types, we're actually copying the returned object — which was locally created in the function — to the object that we're assigning the function to.
For example, let's modify the previous code and make Simple_Rec
nonlimited:
package Non_Limited_Types is type Integer_Access is access Integer; type Simple_Rec is private; function Init (I : Integer) return Simple_Rec; private type Simple_Rec is record V : Integer_Access; end record; end Non_Limited_Types;with Ada.Text_IO; use Ada.Text_IO; with System; with System.Address_Image; package body Non_Limited_Types is function Init (I : Integer) return Simple_Rec is begin return E : Simple_Rec do E.V := new Integer'(I); Put_Line ("E'Address (Init): " & System.Address_Image (E'Address)); end return; end Init; end Non_Limited_Types;with Ada.Text_IO; use Ada.Text_IO; with System; with System.Address_Image; with Non_Limited_Types; use Non_Limited_Types; procedure Show_Non_Limited_Init_By_Copy is A, B : Simple_Rec; begin declare A : Simple_Rec := Init (0); begin Put_Line ("A'Address (local): " & System.Address_Image (A'Address)); end; Put_Line ("----"); declare B : Simple_Rec := Init (0); begin Put_Line ("B'Address (local): " & System.Address_Image (B'Address)); end; end Show_Non_Limited_Init_By_Copy;In this case, we see that the local object
E
in theInit
function is not the same as the object it's being assigned to in theShow_Non_Limited_Init_By_Copy
procedure. In fact,E
is being copied toA
andB
.
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:
with Ada.Strings.Unbounded; use Ada.Strings.Unbounded; package P is task type Some_Task_Type; protected type Some_Protected_Type is -- dummy type end Some_Protected_Type; type T is limited private; function Make_T (Name : String) return T; -- ^^^^^^ -- constructor function private type T is limited record Name : Unbounded_String; My_Task : Some_Task_Type; My_Prot : Some_Protected_Type; end record; end P;package body P is task body Some_Task_Type is begin null; end Some_Task_Type; protected body Some_Protected_Type is end Some_Protected_Type; function Make_T (Name : String) return T is begin return (Name => To_Unbounded_String (Name), others => <>); end Make_T; end P;package P.Aux is function Make_Rumplestiltskin return T; end P.Aux;package body P.Aux is function Make_Rumplestiltskin return T is begin return Make_T (Name => "Rumplestiltskin"); end Make_Rumplestiltskin; end P.Aux;
It is useful to consider the various contexts in which these functions may be called. We've already seen things like:
with P; use P; with P.Aux; use P.Aux; procedure Show_Rumplestiltskin_Constructor is Rumplestiltskin_Is_My_Name : constant T := Make_Rumplestiltskin; begin null; end Show_Rumplestiltskin_Constructor;
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:
with P; use P; with P.Aux; use P.Aux; procedure Show_Parameter_Constructor is procedure Do_Something (X : T) is null; begin Do_Something (X => Make_Rumplestiltskin); end Show_Parameter_Constructor;
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:
with P; use P; with P.Aux; use P.Aux; procedure Show_Heap_Constructor is type T_Ref is access all T; Global : T_Ref; procedure Heap_Alloc is Local : T_Ref; To_Global : Boolean := True; begin Local := new T'(Make_Rumplestiltskin); if To_Global then Global := Local; end if; end Heap_Alloc; begin null; end Show_Heap_Constructor;
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:
with P; use P; with P.Aux; use P.Aux; procedure Show_Outer_Type is type Outer_Type is limited record This : T; That : T; end record; Outer_Obj : Outer_Type := (This => Make_Rumplestiltskin, That => Make_T (Name => "")); begin null; end Show_Outer_Type;
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:
with P; use P; with P.Aux; use P.Aux; procedure Show_Illegal_Constructor is Rumplestiltskin_Is_My_Name : T; begin Rumplestiltskin_Is_My_Name := Make_T (Name => ""); -- Illegal! end Show_Illegal_Constructor;
which is illegal because assignment statements involve copying. Likewise, we can't copy a limited object into some other object:
with P; use P; with P.Aux; use P.Aux; procedure Show_Illegal_Constructor is Rumplestiltskin_Is_My_Name : constant T := Make_T (Name => ""); Other : T := Rumplestiltskin_Is_My_Name; -- Illegal! begin null; end Show_Illegal_Constructor;
Limited types as parameter
Previously, we saw that parameters can be passed by copy or by reference. Also, we discussed the concept of by-copy and by-reference types. Explicitly limited types are by-reference types. Consequently, parameters of these types are always passed by reference.
For further reading...
As an example of the importance of this rule, consider the case of a lock
(as an abstract data type). If
such a lock object were passed by copy, the Acquire
and
Release
operations would be working on copies of this object, not on
the original one. This would lead to timing-dependent bugs.
Let's reuse an example of an explicitly limited type:
package Simple_Recs is type Rec is limited record I : Integer; end record; end Simple_Recs;
In this example, Rec
is a by-reference type because the type declaration
is an explicit limited record. Therefore, the parameter R
of the
Proc
procedure is passed by reference.
We can run the Test
application below and compare the address of the
R
object from Test
to the address of the R
parameter of
Proc
to determine whether both R
s refer to the same object or
not:
with System; package Simple_Recs is type Rec is limited record I : Integer; end record; procedure Proc (R : in out Rec; A : out System.Address); end Simple_Recs;package body Simple_Recs is procedure Proc (R : in out Rec; A : out System.Address) is begin R.I := 0; A := R'Address; end Proc; end Simple_Recs;with Ada.Text_IO; use Ada.Text_IO; with System; use System; with System.Address_Image; with Simple_Recs; use Simple_Recs; procedure Test is R : Rec; AR_Proc, AR_Test : System.Address; begin AR_Proc := R'Address; Proc (R, AR_Test); Put_Line ("R'Address (Proc): " & System.Address_Image (AR_Proc)); Put_Line ("R'Address (Test): " & System.Address_Image (AR_Test)); if AR_Proc = AR_Test then Put_Line ("R was passed by reference."); else Put_Line ("R was passed by copy."); end if; end Test;
When running the Test
application, we confirm that R
was passed
by reference. Note, however, that the fact that R
was passed by
reference doesn't automatically imply that Rec
is a by-reference type:
the type could have been ambiguous, and the compiler could have just decided to
pass the parameter by reference in this case.
Therefore, we have to rely on the rules specified in the Ada Reference Manual:
If a limited type is explicitly limited, a parameter of this type is a by-reference type.
The rule applies to all kinds of explicitly limited types. For example, consider private limited types where the type is declared limited in the private type's completion (in the package's private part): a parameter of this type is a by-reference type.
If a limited type is not explicitly limited, a parameter of this type is neither a by-copy nor a by-reference type.
In this case, the decision whether the parameter is passed by reference or by copy is made by the compiler.
In the Ada Reference Manual