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 consider 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.

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.)

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, "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:

  • explicitly limited types

  • tagged limited types (i.e. with the keyword limited);

  • tagged limited private type;

  • 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 and Discriminants

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.

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;