Access Types

We discussed access types back in the Introduction to Ada course. In this chapter, we discuss further details about access types and techniques when using them. Before we dig into details, however, we're going to make sure we understand the terminology.

Access types: Terminology

In this section, we discuss some of the terminology associated with access types. Usually, the terms used in Ada when discussing references and dynamic memory allocation are different than the ones you might encounter in other languages, so it's necessary you understand what each term means.

Access type, designated subtype and profile

The first term we encounter is (obviously) access type, which is a type that provides us access to an object or a subprogram. We declare access types by using the access keyword:

    
    
    
        
package Show_Access_Type_Declaration is -- -- Declaring access types: -- -- Access-to-object type type Integer_Access is access Integer; -- Access-to-subprogram type type Init_Integer_Access is access function return Integer; end Show_Access_Type_Declaration;

Here, we're declaring two access types: the access-to-object type Integer_Access and the access-to-subprogram type Init_Integer_Access. (We discuss access-to-subprogram types later on).

In the declaration of an access type, we always specify — after the access keyword — the kind of thing we want to designate. In the case of an access-to-object type declaration, we declare a subtype we want to access, which is known as the designated subtype of an access type. In the case of an access-to-subprogram type declaration, the subprogram prototype is known as the designated profile.

In our previous code example, Integer is the designated subtype of the Integer_Access type, and function return Integer is the designated profile of the Init_Integer_Access type.

Important

In contrast to other programming languages, an access type is not a pointer, and it doesn't just indicate an address in memory. We discuss more about addresses later on.

Access object and designated object

We use an access-to-object type by first declaring a variable (or constant) of an access type and then allocating an object. (This is actually just one way of using access types; we discuss other methods later in this chapter.) The actual variable or constant of an access type is called access object, while the object we allocate (via new) is the designated object.

For example:

    
    
    
        
procedure Show_Simple_Allocation is -- Access-to-object type type Integer_Access is access Integer; -- Access object I1 : Integer_Access; begin I1 := new Integer; -- ^^^^^^^^^^^ allocating an object, -- which becomes the designated -- object for I1 end Show_Simple_Allocation;

In this example, I1 is an access object and the object allocated via new Integer is its designated object.

Access value and designated value

An access object and a designated (allocated) object, both store values. The value of an access object is the access value and the value of a designated object is the designated value. For example:

    
    
    
        
procedure Show_Values is -- Access-to-object type type Integer_Access is access Integer; I1, I2, I3 : Integer_Access; begin I1 := new Integer; I3 := new Integer; -- Copying the access value of I1 to I2 I2 := I1; -- Copying the designated value of I1 I3.all := I1.all; end Show_Values;

In this example, the assignment I2 := I1 copies the access value of I1 to I2. The assignment I3.all := I1.all copies I1's designated value to I3's designated object. (As we already know, .all is used to dereference an access object. We discuss this topic again later in this chapter.)

In the Ada Reference Manual

Access types: Allocation

Ada makes the distinction between pool-specific and general access types, as we'll discuss in this section. Before doing so, however, let's talk about memory allocation.

In general terms, memory can be allocated dynamically on the heap or statically on the stack. (Strictly speaking, both are dynamic allocations, in that they occur at run-time with amounts not previously specified.) For example:

    
    
    
        
procedure Show_Simple_Allocation is -- Declaring access type: type Integer_Access is access Integer; -- Declaring access object: A1 : Integer_Access; begin -- Allocating an Integer object on the heap A1 := new Integer; declare -- Allocating an Integer object on the -- stack I : Integer; begin null; end; end Show_Simple_Allocation;

When we allocate an object on the heap via new, the allocation happens in a memory pool that is associated with the access type. In our code example, there's a memory pool associated with the Integer_Access type, and each new Integer allocates a new integer object in that pool. Therefore, access types of this kind are called pool-specific access types. (We discuss more about these types later.)

It is also possible to access objects that were allocated on the stack. To do that, however, we cannot use pool-specific access types because — as the name suggests — they're only allowed to access objects that were allocated in the specific pool associated with the type. Instead, we have to use general access types in this case:

    
    
    
        
procedure Show_General_Access_Type is -- Declaring general access type: type Integer_Access is access all Integer; -- Declaring access object: A1 : Integer_Access; -- Allocating an Integer object on the -- stack: I : aliased Integer; begin -- Getting access to an Integer object that -- was allocated on the stack A1 := I'Access; end Show_General_Access_Type;

In this example, we declare the general access type Integer_Access and the access object A1. To initialize A1, we write I'Access to get access to an integer object I that was allocated on the stack. (For the moment, don't worry much about these details: we'll talk about general access types again when we introduce the topic of aliased objects later on.)

For further reading...

Note that it is possible to use general access types to allocate objects on the heap:

    
    
    
        
procedure Show_Simple_Allocation is -- Declaring general access type: type Integer_Access is access all Integer; -- Declaring access object: A1 : Integer_Access; begin -- -- Allocating an Integer object on the heap -- and initializing an access object of -- the general access type Integer_Access. -- A1 := new Integer; end Show_Simple_Allocation;

Here, we're using a general access type Integer_Access, but allocating an integer object on the heap.

Important

In many code examples, we have used the Integer type as the designated subtype of the access types — by writing access Integer. Although we have used this specific scalar type, we aren't really limited to those types. In fact, we can use any type as the designated subtype, including user-defined types, composite types, task types and protected types.

In the Ada Reference Manual

Pool-specific access types

We've already discussed many aspects about pool-specific access types. In this section, we recapitulate some of those aspects, and discuss some new details that haven't seen yet.

As we know, we cannot directly assign an object Distance_Miles of type Miles to an object Distance_Meters of type Meters, even if both share a common Float type ancestor. The assignment is only possible if we perform a type conversion from Miles to Meters, or vice-versa — e.g.: Distance_Meters := Meters (Distance_Miles) * Miles_To_Meters_Factor.

Similarly, in the case of pool-specific access types, a direct assignment between objects of different access types isn't possible. However, even if both access types have the same designated subtype (let's say, they are both declared using is access Integer), it's still not possible to perform a type conversion between those access types. The only situation when an access type conversion is allowed is when both types have a common ancestor.

Let's see an example:

    
    
    
        
pragma Ada_2022; with Ada.Text_IO; use Ada.Text_IO; procedure Show_Simple_Allocation is -- Declaring pool-specific access type: type Integer_Access_1 is access Integer; type Integer_Access_2 is access Integer; type Integer_Access_2B is new Integer_Access_2; -- Declaring access object: A1 : Integer_Access_1; A2 : Integer_Access_2; A2B : Integer_Access_2B; begin A1 := new Integer; Put_Line ("A1 : " & A1'Image); Put_Line ("Pool: " & A1'Storage_Pool'Image); A2 := new Integer; Put_Line ("A2: " & A2'Image); Put_Line ("Pool: " & A2'Storage_Pool'Image); -- ERROR: Cannot directly assign access values -- for objects of unrelated access -- types; also, cannot convert between -- these types. -- -- A1 := A2; -- A1 := Integer_Access_1 (A2); A2B := Integer_Access_2B (A2); Put_Line ("A2B: " & A2B'Image); Put_Line ("Pool: " & A2B'Storage_Pool'Image); end Show_Simple_Allocation;

In this example, we declare three access types: Integer_Access_1, Integer_Access_2 and Integer_Access_2B. Also, the Integer_Access_2B type is derived from the Integer_Access_2 type. Therefore, we can convert an object of Integer_Access_2 type to the Integer_Access_2B type — we do this in the A2B := Integer_Access_2B (A2) assignment. However, we cannot directly assign to or convert between unrelated types such as Integer_Access_1 and Integer_Access_2. (We would get a compilation error if we included the A1 := A2 or the A1 := Integer_Access_1 (A2) assignment.)

Important

Remember that:

  • As mentioned in the Introduction to Ada course:

    • an access type can be unconstrained, but the actual object allocation must be constrained;

    • we can use a qualified expression to allocate an object.

  • We can use the Storage_Size attribute to limit the size of the memory pool associated with an access type, as discussed previously in the section about storage size.

  • When running out of memory while allocating via new, we get a Storage_Error exception because of the storage check.

For example:

    
    
    
        
pragma Ada_2022; with Ada.Text_IO; use Ada.Text_IO; procedure Show_Array_Allocation is -- Unconstrained array type: type Integer_Array is array (Positive range <>) of Integer; -- Access type with unconstrained -- designated subtype and limited storage -- size. type Integer_Array_Access is access Integer_Array with Storage_Size => 128; -- An access object: A1 : Integer_Array_Access; procedure Show_Info (IAA : Integer_Array_Access) is begin Put_Line ("Allocated: " & IAA'Image); Put_Line ("Length: " & IAA.all'Length'Image); Put_Line ("Values: " & IAA.all'Image); end Show_Info; begin -- Allocating an integer array with -- constrained range on the heap: A1 := new Integer_Array (1 .. 3); A1.all := [others => 42]; Show_Info (A1); -- Allocating an integer array on the -- heap using a qualified expression: A1 := new Integer_Array'(5, 10); Show_Info (A1); -- A third allocation fails at run time -- because of the constrained storage -- size: A1 := new Integer_Array (1 .. 100); Show_Info (A1); exception when Storage_Error => Put_Line ("Out of memory!"); end Show_Array_Allocation;

Multiple allocation

Up to now, we have seen examples of allocating a single object on the heap. It's possible to allocate multiple objects at once as well — i.e. syntactic sugar is available to simplify the code that performs this allocation. For example:

    
    
    
        
pragma Ada_2022; with Ada.Text_IO; use Ada.Text_IO; procedure Show_Access_Array_Allocation is type Integer_Access is access Integer; type Integer_Access_Array is array (Positive range <>) of Integer_Access; -- An array of access objects: Arr : Integer_Access_Array (1 .. 10); begin -- -- Allocating 10 access objects and -- initializing the corresponding designated -- object with zero: -- Arr := (others => new Integer'(0)); -- Same as: for I in Arr'Range loop Arr (I) := new Integer'(0); end loop; Put_Line ("Arr: " & Arr'Image); Put_Line ("Arr (designated values): "); for E of Arr loop Put (E.all'Image); end loop; end Show_Access_Array_Allocation;

In this example, we have the access type Integer_Access and an array type of this access type (Integer_Access_Array). We also declare an array Arr of Integer_Access_Array type. This means that each component of Arr is an access object. We allocate all ten components of the Arr array by simply writing Arr := (others => new Integer). This array aggregate is syntactic sugar for a loop over Arr that allocates each component. (Note that, by writing Arr := (others => new Integer'(0)), we're also initializing the designated objects with zero.)

Let's see another code example, this time with task types:

    
    
    
        
package Workers is task type Worker is entry Start (Id : Positive); entry Stop; end Worker; type Worker_Access is access Worker; type Worker_Array is array (Positive range <>) of Worker_Access; end Workers;
with Ada.Text_IO; use Ada.Text_IO; package body Workers is task body Worker is Id : Positive; begin accept Start (Id : Positive) do Worker.Id := Id; end Start; Put_Line ("Started Worker #" & Id'Image); accept Stop; Put_Line ("Stopped Worker #" & Id'Image); end Worker; end Workers;
with Ada.Text_IO; use Ada.Text_IO; with Workers; use Workers; procedure Show_Workers is Worker_Arr : Worker_Array (1 .. 20); begin -- -- Allocating 20 workers at once: -- Worker_Arr := (others => new Worker); for I in Worker_Arr'Range loop Worker_Arr (I).Start (I); end loop; Put_Line ("Some processing..."); delay 1.0; for W of Worker_Arr loop W.Stop; end loop; end Show_Workers;

In this example, we declare the task type Worker, the access type Worker_Access and an array of access to tasks Worker_Array. Using this approach, a task is only created when we allocate an individual component of an array of Worker_Array type. Thus, when we declare the Worker_Arr array in this example, we're only preparing a container of 20 workers, but we don't have any actual tasks yet. We bring the 20 tasks into existence by writing Worker_Arr := (others => new Worker).

Discriminants as Access Values

We can use access types when declaring discriminants. Let's see an example:

    
    
    
        
package Custom_Recs is -- Declaring an access type: type Integer_Access is access Integer; -- Declaring a discriminant with this -- access type: type Rec (IA : Integer_Access) is record I : Integer := IA.all; -- ^^^^^^^^^ -- Setting I's default to use the -- designated value of IA: end record; procedure Show (R : Rec); end Custom_Recs;
with Ada.Text_IO; use Ada.Text_IO; package body Custom_Recs is procedure Show (R : Rec) is begin Put_Line ("R.IA = " & Integer'Image (R.IA.all)); Put_Line ("R.I = " & Integer'Image (R.I)); end Show; end Custom_Recs;
with Custom_Recs; use Custom_Recs; procedure Show_Discriminants_As_Access_Values is IA : constant Integer_Access := new Integer'(10); R : Rec (IA); begin Show (R); IA.all := 20; R.I := 30; Show (R); -- As expected, we cannot change the -- discriminant. The following line is -- triggers a compilation error: -- -- R.IA := new Integer; end Show_Discriminants_As_Access_Values;

In the Custom_Recs package from this example, we declare the access type Integer_Access. We then use this type to declare the discriminant (IA) of the Rec type. In the Show_Discriminants_As_Access_Values procedure, we see that (as expected) we cannot change the discriminant of an object of Rec type: an assignment such as R.IA := new Integer would trigger a compilation error.

Note that we can use a default for the discriminant:

    
    
    
        
package Custom_Recs is type Integer_Access is access Integer; type Rec (IA : Integer_Access := new Integer'(0)) is -- ^^^^^^^^^^^^^^^ -- default value record I : Integer := IA.all; end record; procedure Show (R : Rec); end Custom_Recs;
with Custom_Recs; use Custom_Recs; procedure Show_Discriminants_As_Access_Values is R1 : Rec; -- ^^^ -- no discriminant: use default R2 : Rec (new Integer'(20)); -- ^^^^^^^^^^^^^^^^ -- allocating an unnamed integer object begin Show (R1); Show (R2); end Show_Discriminants_As_Access_Values;

Here, we've changed the declaration of the Rec type to allocate an integer object if the type's discriminant isn't provided — we can see this in the declaration of the R1 object in the Show_Discriminants_As_Access_Values procedure. Also, in this procedure, we're allocating an unnamed integer object in the declaration of R2.

Unconstrained type as designated subtype

Notice that we were using a scalar type as the designated subtype of the Integer_Access type. We could have used an unconstrained type as well. In fact, this is often used for the sake of having the effect of an unconstrained discriminant type.

Let's see an example:

    
    
    
        
package Persons is -- Declaring an access type whose -- designated subtype is unconstrained: type String_Access is access String; -- Declaring a discriminant with this -- access type: type Person (Name : String_Access) is record Age : Integer; end record; procedure Show (P : Person); end Persons;
with Ada.Text_IO; use Ada.Text_IO; package body Persons is procedure Show (P : Person) is begin Put_Line ("Name = " & P.Name.all); Put_Line ("Age = " & Integer'Image (P.Age)); end Show; end Persons;
with Persons; use Persons; procedure Show_Person is P : Person (new String'("John")); begin P.Age := 30; Show (P); end Show_Person;

In this example, the discriminant of the Person type has an unconstrained designated type. In the Show_Person procedure, we declare the P object and specify the constraints of the allocated string object — in this case, a four-character string initialized with the name "John".

For further reading...

In the previous code example, we used an array — actually, a string — to demonstrate the advantage of using discriminants as access values, for we can use an unconstrained type as the designated subtype. In fact, as we discussed earlier in another chapter, we can only use discrete types (or access types) as discriminants. Therefore, you wouldn't be able to use a string, for example, directly as a discriminant without using access types:

    
    
    
        
package Persons is -- ERROR: Declaring a discriminant with an -- unconstrained type: type Person (Name : String) is record Age : Integer; end record; end Persons;

As expected, compilation fails for this code because the discriminant of the Person type is indefinite.

However, the advantage of discriminants as access values isn't restricted to being able to use unconstrained types such as arrays: we could really use any type as the designated subtype! In fact, we can generalized this to:

    
    
    
        
generic type T (<>); -- any type type T_Access is access T; package Gen_Custom_Recs is -- Declare a type whose discriminant D can -- access any type: type T_Rec (D : T_Access) is null record; end Gen_Custom_Recs;
with Gen_Custom_Recs; package Custom_Recs is type Incomp; -- Incomplete type declaration! type Incomp_Access is access Incomp; -- Instantiating package using -- incomplete type Incomp: package Inst is new Gen_Custom_Recs (T => Incomp, T_Access => Incomp_Access); subtype Rec is Inst.T_Rec; -- At this point, Rec (Inst.T_Rec) uses -- an incomplete type as the designated -- subtype of its discriminant type procedure Show (R : Rec) is null; -- Now, we complete the Incomp type: type Incomp (B : Boolean := True) is private; private -- Finally, we have the full view of the -- Incomp type: type Incomp (B : Boolean := True) is null record; end Custom_Recs;
with Custom_Recs; use Custom_Recs; procedure Show_Rec is R : Rec (new Incomp); begin Show (R); end Show_Rec;

In the Gen_Custom_Recs package, we're using type T (<>) — which can be any type — for the designated subtype of the access type T_Access, which is the type of T_Rec's discriminant. In the Custom_Recs package, we use the incomplete type Incomp to instantiate the generic package. Only after the instantiation, we declare the complete type.

Later on, we'll discuss discriminants again when we look into anonymous access discriminants, which provide some advantages in terms of accessibility rules.

Whole object assignments

As expected, we cannot change the discriminant value in whole object assignments. If we do that, the Constraint_Error exception is raised at runtime:

    
    
    
        
with Persons; use Persons; procedure Show_Person is S1 : String_Access := new String'("John"); S2 : String_Access := new String'("Mark"); P : Person := (Name => S1, Age => 30); begin P := (Name => S1, Age => 31); -- ^^ OK: we didn't change the -- discriminant. Show (P); -- We can just repeat the discriminant: P := (Name => P.Name, Age => 32); -- ^^^^^^ OK: we didn't change the -- discriminant. Show (P); -- Of course, we can change the string itself: S1.all := "Mark"; Show (P); P := (Name => S2, Age => 40); -- ^^ ERROR: we changed the -- discriminant! Show (P); end Show_Person;

The first and the second assignments to P are OK because we didn't change the discriminant. However, the last assignment raises the Constraint_Error exception at runtime because we're changing the discriminant.

Parameters as Access Values

In addition to using discriminants as access values, we can use access types for subprogram formal parameters. For example, the N parameter of the Show procedure below has an access type:

    
    
    
        
package Names is type Name is access String; procedure Show (N : Name); end Names;

This is the complete code example:

    
    
    
        
package Names is type Name is access String; procedure Show (N : Name); end Names;
with Ada.Text_IO; use Ada.Text_IO; package body Names is procedure Show (N : Name) is begin Put_Line ("Name: " & N.all); end Show; end Names;
with Names; use Names; procedure Show_Names is N : Name := new String'("John"); begin Show (N); end Show_Names;

Note that in this example, the Show procedure is basically just displaying the string. Since the procedure isn't doing anything that justifies the need for an access type, we could have implemented it with a simpler type:

    
    
    
        
package Names is type Name is access String; procedure Show (N : String); end Names;
with Ada.Text_IO; use Ada.Text_IO; package body Names is procedure Show (N : String) is begin Put_Line ("Name: " & N); end Show; end Names;
with Names; use Names; procedure Show_Names is N : Name := new String'("John"); begin Show (N.all); end Show_Names;

It's important to highlight the difference between passing an access value to a subprogram and passing an object by reference. In both versions of this code example, the compiler will make use of a reference for the actual parameter of the N parameter of the Show procedure. However, the difference between these two cases is that:

  • N : Name is a reference to an object (because it's an access value) that is passed by value, and

  • N : String is an object passed by reference.

Changing the referenced object

Since the Name type gives us access to an object in the Show procedure, we could actually change this object inside the procedure. To illustrate this, let's change the Show procedure to lower each character of the string before displaying it (and rename the procedure to Lower_And_Show):

    
    
    
        
package Names is type Name is access String; procedure Lower_And_Show (N : Name); end Names;
with Ada.Text_IO; use Ada.Text_IO; with Ada.Characters.Handling; use Ada.Characters.Handling; package body Names is procedure Lower_And_Show (N : Name) is begin for I in N'Range loop N (I) := To_Lower (N (I)); end loop; Put_Line ("Name: " & N.all); end Lower_And_Show; end Names;
with Names; use Names; procedure Show_Changed_Names is N : Name := new String'("John"); begin Lower_And_Show (N); end Show_Changed_Names;

Notice that, again, we could have implemented the Lower_And_Show procedure without using an access type:

    
    
    
        
package Names is type Name is access String; procedure Lower_And_Show (N : in out String); end Names;
with Ada.Text_IO; use Ada.Text_IO; with Ada.Characters.Handling; use Ada.Characters.Handling; package body Names is procedure Lower_And_Show (N : in out String) is begin for I in N'Range loop N (I) := To_Lower (N (I)); end loop; Put_Line ("Name: " & N); end Lower_And_Show; end Names;
with Names; use Names; procedure Show_Changed_Names is N : Name := new String'("John"); begin Lower_And_Show (N.all); end Show_Changed_Names;

Replace the access value

Instead of changing the object in the Lower_And_Show procedure, we could replace the access value by another one — for example, by allocating a new string inside the procedure. In this case, we have to pass the access value by reference using the in out parameter mode:

    
    
    
        
package Names is type Name is access String; procedure Lower_And_Show (N : in out Name); end Names;
with Ada.Text_IO; use Ada.Text_IO; with Ada.Characters.Handling; use Ada.Characters.Handling; package body Names is procedure Lower_And_Show (N : in out Name) is begin N := new String'(To_Lower (N.all)); Put_Line ("Name: " & N.all); end Lower_And_Show; end Names;
with Names; use Names; procedure Show_Changed_Names is N : Name := new String'("John"); begin Lower_And_Show (N); end Show_Changed_Names;

Now, instead of changing the object referenced by N, we're actually replacing it with a new object that we allocate inside the Lower_And_Show procedure.

As expected, contrary to the previous examples, we cannot implement this code by relying on parameter modes to replace the object. In fact, we have to use access types for this kind of operations.

Note that this implementation creates a memory leak. In a proper implementation, we should make sure to deallocate the object, as explained later on.

Side-effects on designated objects

In previous code examples from this section, we've seen that passing a parameter by reference using the in or in out parameter modes is an alternative to using access values as parameters. Let's focus on the subprogram declarations of those code examples and their parameter modes:

Subprogram

Parameter type

Parameter mode

Show

Name

in

Show

String

in

Lower_And_Show

Name

in

Lower_And_Show

String

in out

When we analyze the information from this table, we see that in the case of using strings with different parameter modes, we have a clear indication whether the subprogram might change the object or not. For example, we know that a call to Show (N : String) won't change the string object that we're passing as the actual parameter.

In the case of passing an access value, we cannot know whether the designated object is going to be altered by a call to the subprogram. In fact, in both Show and Lower_And_Show procedures, the parameter is the same: N : Name — in other words, the parameter mode is in in both cases. Here, there's no clear indication about the effects of a subprogram call on the designated object.

The simplest way to ensure that the object isn't changed in the subprogram is by using access-to-constant types, which we discuss later on. In this case, we're basically saying that the object we're accessing in Show is constant, so we cannot possibly change it:

    
    
    
        
package Names is type Name is access String; type Constant_Name is access constant String; procedure Show (N : Constant_Name); end Names;
with Ada.Text_IO; use Ada.Text_IO; -- with Ada.Characters.Handling; -- use Ada.Characters.Handling; package body Names is procedure Show (N : Constant_Name) is begin -- for I in N'Range loop -- N (I) := To_Lower (N (I)); -- end loop; Put_Line ("Name: " & N.all); end Show; end Names;
with Names; use Names; procedure Show_Names is N : Name := new String'("John"); begin Show (Constant_Name (N)); end Show_Names;

In this case, the Constant_Name type ensures that the N parameter won't be changed in the Show procedure. Note that we need to convert from Name to Constant_Name to be able to call the Show procedure (in the Show_Names procedure). Although using in String is still a simpler solution, this approach works fine.

(Feel free to uncomment the call to To_Lower in the Show procedure and the corresponding with- and use-clauses to see that the compilation fails when trying to change the constant object.)

We could also mitigate the problem by using contracts. For example:

    
    
    
        
package Names is type Name is access String; procedure Show (N : Name) with Post => N.all'Old = N.all; -- ^^^^^^^^^^^^^^^^^ -- we promise that we won't change -- the object end Names;
with Ada.Text_IO; use Ada.Text_IO; -- with Ada.Characters.Handling; -- use Ada.Characters.Handling; package body Names is procedure Show (N : Name) is begin -- for I in N'Range loop -- N (I) := To_Lower (N (I)); -- end loop; Put_Line ("Name: " & N.all); end Show; end Names;
with Names; use Names; procedure Show_Names is N : Name := new String'("John"); begin Show (N); end Show_Names;

Although a bit more verbose than a simple in String, the information in the specification of Show at least gives us an indication that the object won't be affected by the call to this subprogram. Note that this code actually compiles if we try to modify N.all in the Show procedure, but the post-condition fails at runtime when we do that.

(By uncommentating and building the code again, you'll see an exception being raised at runtime when trying to change the object.)

In the postcondition above, we're using 'Old to refer to the original object before the subprogram call. Unfortunately, we cannot use this attribute when dealing with limited private types — or limited types in general. For example, let's change the declaration of Name and have it as a limited private type instead:

    
    
    
        
package Names is type Name is limited private; function Init (S : String) return Name; function Equal (N1, N2 : Name) return Boolean; procedure Show (N : Name) with Post => Equal (N'Old = N); private type Name is access String; function Init (S : String) return Name is (new String'(S)); function Equal (N1, N2 : Name) return Boolean is (N1.all = N2.all); end Names;
with Ada.Text_IO; use Ada.Text_IO; -- with Ada.Characters.Handling; -- use Ada.Characters.Handling; package body Names is procedure Show (N : Name) is begin -- for I in N'Range loop -- N (I) := To_Lower (N (I)); -- end loop; Put_Line ("Name: " & N.all); end Show; end Names;
with Names; use Names; procedure Show_Names is N : Name := Init ("John"); begin Show (N); end Show_Names;

In this case, we have no means to indicate that a call to Show won't change the internal state of the actual parameter.

For further reading...

As an alternative, we could declare a new Constant_Name type that is also limited private. If we use this type in Show procedure, we're at least indicating (in the type name) that the type is supposed to be constant — even though we're not directly providing means to actually ensure that no modifications occur in a call to the procedure. However, the fact that we declare this type as an access-to-constant (in the private part of the specification) makes it clear that a call to Show won't change the designated object.

Let's look at the adapted code:

    
    
    
        
package Names is type Name is limited private; type Constant_Name is limited private; function Init (S : String) return Name; function To_Constant_Name (N : Name) return Constant_Name; procedure Show (N : Constant_Name); private type Name is access String; type Constant_Name is access constant String; function Init (S : String) return Name is (new String'(S)); function To_Constant_Name (N : Name) return Constant_Name is (Constant_Name (N)); end Names;
with Ada.Text_IO; use Ada.Text_IO; -- with Ada.Characters.Handling; -- use Ada.Characters.Handling; package body Names is procedure Show (N : Constant_Name) is begin -- for I in N'Range loop -- N (I) := To_Lower (N (I)); -- end loop; Put_Line ("Name: " & N.all); end Show; end Names;
with Names; use Names; procedure Show_Names is N : Name := Init ("John"); begin Show (To_Constant_Name (N)); end Show_Names;

In this version of the source code, the Show procedure doesn't have any side-effects, as we cannot modify N inside the procedure.

Having the information about the effects of a subprogram call to an object is very important: we can use this information to set expectations — and avoid unexpected changes to an object. Also, this information can be used to prove that a program works as expected. Therefore, whenever possible, we should avoid access values as parameters. Instead, we can rely on appropriate parameter modes and pass an object by reference.

There are cases, however, where the design of our application doesn't permit replacing the access type with simple parameter modes. Whenever we have an abstract data type encapsulated as a limited private type — such as in the last code example —, we might have no means to avoid access values as parameters. In this case, using the access type is of course justifiable. We'll see such a case in the next section.

Self-reference

As we've discussed in the section about incomplete types <Adv_Ada_Incomplete_Types>, we can use incomplete types to create a recursive, self-referencing type. Let's revisit a code example from that section:

    
    
    
        
package Linked_List_Example is type Integer_List; type Next is access Integer_List; type Integer_List is record I : Integer; N : Next; end record; end Linked_List_Example;

Here, we're using the incomplete type Integer_List in the declaration of the Next type, which we then use in the complete declaration of the Integer_List type.

Self-references are useful, for example, to create unbounded containers — such as the linked lists mentioned in the example above. Let's extend this code example and partially implement a generic package for linked lists:

    
    
    
        
generic type T is private; package Linked_Lists is type List is limited private; procedure Append_Front (L : in out List; E : T); procedure Append_Rear (L : in out List; E : T); procedure Show (L : List); private -- Incomplete type declaration: type Component; -- Using incomplete type: type List is access Component; type Component is record Value : T; Next : List; -- ^^^^ -- Self-reference via access type end record; end Linked_Lists;
pragma Ada_2022; with Ada.Text_IO; use Ada.Text_IO; package body Linked_Lists is procedure Append_Front (L : in out List; E : T) is New_First : constant List := new Component'(Value => E, Next => L); begin L := New_First; end Append_Front; procedure Append_Rear (L : in out List; E : T) is New_Last : constant List := new Component'(Value => E, Next => null); begin if L = null then L := New_Last; else declare Last : List := L; begin while Last.Next /= null loop Last := Last.Next; end loop; Last.Next := New_Last; end; end if; end Append_Rear; procedure Show (L : List) is Curr : List := L; begin if L = null then Put_Line ("[ ]"); else Put ("["); loop Put (Curr.Value'Image); Put (" "); exit when Curr.Next = null; Curr := Curr.Next; end loop; Put_Line ("]"); end if; end Show; end Linked_Lists;
with Linked_Lists; procedure Test_Linked_List is package Integer_Lists is new Linked_Lists (T => Integer); use Integer_Lists; L : List; begin Append_Front (L, 3); Append_Rear (L, 4); Append_Rear (L, 5); Append_Front (L, 2); Append_Front (L, 1); Append_Rear (L, 6); Append_Rear (L, 7); Show (L); end Test_Linked_List;

In this example, we declare an incomplete type Component in the private part of the generic Linked_Lists package. We use this incomplete type to declare the access type List, which is then used as a self-reference in the Next component of the Component type.

Note that we're using the List type as a parameter for the Append_Front, Append_Rear and Show procedures.

In the Ada Reference Manual

Dereferencing

In the Introduction to Ada course, we discussed the .all syntax to dereference access values:

    
    
    
        
with Ada.Text_IO; use Ada.Text_IO; procedure Show_Dereferencing is -- Declaring access type: type Integer_Access is access Integer; -- Declaring access object: A1 : Integer_Access; begin A1 := new Integer; -- Dereferencing access value: A1.all := 22; Put_Line ("A1: " & Integer'Image (A1.all)); end Show_Dereferencing;

In this example, we declare A1 as an access object, which allows us to access objects of Integer type. We dereference A1 by writing A1.all.

Here's another example, this time with an array:

    
    
    
        
with Ada.Text_IO; use Ada.Text_IO; procedure Show_Dereferencing is type Integer_Array is array (Positive range <>) of Integer; type Integer_Array_Access is access Integer_Array; Arr : constant Integer_Array_Access := new Integer_Array (1 .. 6); begin Arr.all := (1, 2, 3, 5, 8, 13); for I in Arr'Range loop Put_Line ("Arr (: " & Integer'Image (I) & "): " & Integer'Image (Arr.all (I))); end loop; end Show_Dereferencing;

In this example, we dereference the access value by writing Arr.all. We then assign an array aggregate to it — this becomes Arr.all := (..., ...);. Similarly, in the loop, we write Arr.all (I) to access the I component of the array.

In the Ada Reference Manual

Implicit Dereferencing

Implicit dereferencing allows us to omit the .all suffix without getting a compilation error. In this case, the compiler knows that the dereferenced object is implied, not the access value.

Ada supports implicit dereferencing in these use cases:

  • when accessing components of a record or an array — including array slices.

  • when accessing subprograms that have at least one parameter (we discuss this topic later in this chapter);

  • when accessing some attributes — such as some array and task attributes.

Arrays

Let's start by looking into an example of implicit dereferencing of arrays. We can take the previous code example and replace Arr.all (I) by Arr (I):

    
    
    
        
with Ada.Text_IO; use Ada.Text_IO; procedure Show_Dereferencing is type Integer_Array is array (Positive range <>) of Integer; type Integer_Array_Access is access Integer_Array; Arr : constant Integer_Array_Access := new Integer_Array (1 .. 6); begin Arr.all := (1, 2, 3, 5, 8, 13); Arr (1 .. 6) := (1, 2, 3, 5, 8, 13); for I in Arr'Range loop Put_Line ("Arr (: " & Integer'Image (I) & "): " & Integer'Image (Arr (I))); -- ^ .all is implicit. end loop; end Show_Dereferencing;

Both forms — Arr.all (I) and Arr (I) — are equivalent. Note, however, that there's no implicit dereferencing when we want to access the whole array. (Therefore, we cannot write Arr := (1, 2, 3, 5, 8, 13);.) However, as slices are implicitly dereferenced, we can write Arr (1 .. 6) := (1, 2, 3, 5, 8, 13); instead of Arr.all (1 .. 6) := (1, 2, 3, 5, 8, 13);. Alternatively, we can assign to the array components individually and use implicit dereferencing for each component:

Arr (1) := 1;
Arr (2) := 2;
Arr (3) := 3;
Arr (4) := 5;
Arr (5) := 8;
Arr (6) := 13;

Implicit dereferencing isn't available for the whole array because we have to distinguish between assigning to access objects and assigning to actual arrays. For example:

    
    
    
        
with Ada.Text_IO; use Ada.Text_IO; procedure Show_Array_Assignments is type Integer_Array is array (Positive range <>) of Integer; type Integer_Array_Access is access Integer_Array; procedure Show_Array (Name : String; Arr : Integer_Array_Access) is begin Put (Name); for E of Arr.all loop Put (Integer'Image (E)); end loop; New_Line; end Show_Array; Arr_1 : constant Integer_Array_Access := new Integer_Array (1 .. 6); Arr_2 : Integer_Array_Access := new Integer_Array (1 .. 6); begin Arr_1.all := (1, 2, 3, 5, 8, 13); Arr_2.all := (21, 34, 55, 89, 144, 233); -- Array assignment Arr_2.all := Arr_1.all; Show_Array ("Arr_2", Arr_2); -- Access value assignment Arr_2 := Arr_1; Arr_1.all := (377, 610, 987, 1597, 2584, 4181); Show_Array ("Arr_2", Arr_2); end Show_Array_Assignments;

Here, Arr_2.all := Arr_1.all is an array assignment, while Arr_2 := Arr_1 is an access value assignment. By forcing the usage of the .all suffix, the distinction is clear. Implicit dereferencing, however, could be confusing here. (For example, the .all suffix in Arr_2 := Arr_1.all is an oversight by the programmer when the intention actually was to use access values on both sides.) Therefore, implicit dereferencing is only supported in those cases where there's no risk of ambiguities or oversights.

Records

Let's see an example of implicit dereferencing of a record:

    
    
    
        
with Ada.Text_IO; use Ada.Text_IO; procedure Show_Dereferencing is type Rec is record I : Integer; F : Float; end record; type Rec_Access is access Rec; R : constant Rec_Access := new Rec; begin R.all := (I => 1, F => 5.0); Put_Line ("R.I: " & Integer'Image (R.I)); Put_Line ("R.F: " & Float'Image (R.F)); end Show_Dereferencing;

Again, we can replace R.all.I by R.I, as record components are implicitly dereferenced. Also, we could use implicit dereference when assigning to record components individually:

R.I := 1;
R.F := 5.0;

However, we have to write R.all when assigning to the whole record R.

Attributes

Finally, let's see an example of implicit dereference when using attributes:

    
    
    
        
with Ada.Text_IO; use Ada.Text_IO; procedure Show_Dereferencing is type Integer_Array is array (Positive range <>) of Integer; type Integer_Array_Access is access Integer_Array; Arr : constant Integer_Array_Access := new Integer_Array (1 .. 6); begin Put_Line ("Arr'First: " & Integer'Image (Arr'First)); Put_Line ("Arr'Last: " & Integer'Image (Arr'Last)); Put_Line ("Arr'Component_Size: " & Integer'Image (Arr'Component_Size)); Put_Line ("Arr.all'Component_Size: " & Integer'Image (Arr.all'Component_Size)); Put_Line ("Arr'Size: " & Integer'Image (Arr'Size)); Put_Line ("Arr.all'Size: " & Integer'Image (Arr.all'Size)); end Show_Dereferencing;

Here, we can write Arr'First and Arr'Last instead of Arr.all'First and Arr.all'Last, respectively, because Arr is implicitly dereferenced. The same applies to Arr'Component_Size. Note that we can write both Arr'Size and Arr.all'Size, but they have different meanings:

  • Arr'Size is the size of the access object; while

  • Arr.all'Size indicates the size of the actual array Arr.

In other words, the Size attribute is not implicitly dereferenced. In fact, any attribute that could potentially be ambiguous is not implicitly dereferenced. Therefore, in those cases, we must explicitly indicate (by using .all or not) how we want to use the attribute.

Summary

The following table summarizes all instances where implicit dereferencing is supported:

Entities

Standard Usage

Implicit Dereference

Array components

Arr.all (I)

Arr (I)

Array slices

Arr.all (F .. L)

Arr (F .. L)

Record components

Rec.all.C

Rec.C

Array attributes

Arr.all’First

Arr’First

Arr.all’First (N)

Arr’First (N)

Arr.all’Last

Arr’Last

Arr.all’Last (N)

Arr’Last (N)

Arr.all’Range

Arr’Range

Arr.all’Range (N)

Arr’Range (N)

Arr.all’Length

Arr’Length

Arr.all’Length (N)

Arr’Length (N)

Arr.all’Component_Size

Arr’Component_Size

Task attributes

T.all'Identity

T'Identity

T.all'Storage_Size

T'Storage_Size

T.all'Terminated

T'Terminated

T.all'Callable

T'Callable

Tagged type attributes

X.all’Tag

X’Tag

Other attributes

X.all'Valid

X'Valid

X.all'Old

X'Old

A.all’Constrained

A’Constrained

Ragged arrays

Ragged arrays — also known as jagged arrays — are non-uniform, multidimensional arrays. They can be useful to implement tables with varying number of coefficients, as we discuss as an example in this section.

Uniform multidimensional arrays

Consider an algorithm that processes data based on coefficients that depends on a selected quality level:

Quality level

Number of coefficients

#1

#2

#3

#4

#5

Simplified

1

0.15

Better

3

0.02

0.16

0.27

Best

5

0.01

0.08

0.12

0.20

0.34

(Note that this is just a bogus table with no real purpose, as we're not trying to implement any actual algorithm.)

We can implement this table as a two-dimensional array (Calc_Table), where each quality level has an associated array:

    
    
    
        
package Data_Processing is type Quality_Level is (Simplified, Better, Best); private Calc_Table : constant array (Quality_Level, 1 .. 5) of Float := (Simplified => (0.15, 0.00, 0.00, 0.00, 0.00), Better => (0.02, 0.16, 0.27, 0.00, 0.00), Best => (0.01, 0.08, 0.12, 0.20, 0.34)); Last : constant array (Quality_Level) of Positive := (Simplified => 1, Better => 3, Best => 5); end Data_Processing;

Note that, in this implementation, we have a separate table Last that indicates the actual number of coefficients of each quality level.

Alternatively, we could use a record (Table_Coefficient) that stores the number of coefficients and the actual coefficients:

    
    
    
        
package Data_Processing is type Quality_Level is (Simplified, Better, Best); type Data is array (Positive range <>) of Float; private type Table_Coefficient is record Last : Positive; Coef : Data (1 .. 5); end record; Calc_Table : constant array (Quality_Level) of Table_Coefficient := (Simplified => (1, (0.15, 0.00, 0.00, 0.00, 0.00)), Better => (3, (0.02, 0.16, 0.27, 0.00, 0.00)), Best => (5, (0.01, 0.08, 0.12, 0.20, 0.34))); end Data_Processing;

In this case, we have a unidimensional array where each component (of Table_Coefficient type) contains an array (Coef) with the coefficients.

This is an example of a Process procedure that references the Calc_Table:

    
    
    
        
package Data_Processing.Operations is procedure Process (D : in out Data; Q : Quality_Level); end Data_Processing.Operations;
package body Data_Processing.Operations is procedure Process (D : in out Data; Q : Quality_Level) is begin for I in D'Range loop for J in 1 .. Calc_Table (Q).Last loop -- ... * Calc_Table (Q).Coef (J) null; end loop; -- D (I) := ... null; end loop; end Process; end Data_Processing.Operations;

Note that, to loop over the coefficients, we're using for J in 1 .. Calc_Table (Q).Last loop instead of for J in Calc_Table (Q)'Range loop. As we're trying to make a non-uniform array fit in a uniform array, we cannot simply loop over all elements using the Range attribute, but must be careful to use the correct number of elements in the loop instead.

Also, note that Calc_Table has 15 coefficients in total. Out of those coefficients, 6 coefficients (or 40 percent of the table) aren't being used. Naturally, this is wasted memory space. We can improve this by using ragged arrays.

Non-uniform multidimensional array

Ragged arrays are declared by using an access type to an array. By doing that, each array can be declared with a different size, thereby creating a non-uniform multidimensional array.

For example, we can declare a constant array Table as a ragged array:

    
    
    
        
package Data_Processing is type Integer_Array is array (Positive range <>) of Integer; private type Integer_Array_Access is access constant Integer_Array; Table : constant array (1 .. 3) of Integer_Array_Access := (1 => new Integer_Array'(1 => 15), 2 => new Integer_Array'(1 => 12, 2 => 15, 3 => 20), 3 => new Integer_Array'(1 => 12, 2 => 15, 3 => 20, 4 => 20, 5 => 25, 6 => 30)); end Data_Processing;

Here, each component of Table is an access to another array. As each array is allocated via new, those arrays may have different sizes.

We can rewrite the example from the previous subsection using a ragged array for the Calc_Table:

    
    
    
        
package Data_Processing is type Quality_Level is (Simplified, Better, Best); type Data is array (Positive range <>) of Float; private type Coefficients is access constant Data; Calc_Table : constant array (Quality_Level) of Coefficients := (Simplified => new Data'(1 => 0.15), Better => new Data'(0.02, 0.16, 0.27), Best => new Data'(0.01, 0.08, 0.12, 0.20, 0.34)); end Data_Processing;

Now, we aren't wasting memory space because each data component has the right size that is required for each quality level. Also, we don't need to store the number of coefficients, as this information is automatically available from the array initialization — via the allocation of the Data array for the Coefficients type.

Note that the Coefficients type is defined as access constant. We discuss access-to-constant types in more details later on.

This is the adapted Process procedure:

    
    
    
        
package Data_Processing.Operations is procedure Process (D : in out Data; Q : Quality_Level); end Data_Processing.Operations;
package body Data_Processing.Operations is procedure Process (D : in out Data; Q : Quality_Level) is begin for I in D'Range loop for J in Calc_Table (Q)'Range loop -- ... * Calc_Table (Q).Coef (J) null; end loop; -- D (I) := ... null; end loop; end Process; end Data_Processing.Operations;

Now, we can simply loop over the coefficients by writing for J in Calc_Table (Q)'Range loop, as each element of Calc_Table automatically has the correct range.

Aliasing

The term aliasing refers to objects in memory that we can access using more than a single reference. In Ada, if we allocate an object via new, we have a potentially aliased object. We can then have multiple references to this object:

    
    
    
        
with Ada.Text_IO; use Ada.Text_IO; procedure Show_Aliasing is type Integer_Access is access Integer; A1, A2 : Integer_Access; begin A1 := new Integer; A2 := A1; A1.all := 22; Put_Line ("A1: " & Integer'Image (A1.all)); Put_Line ("A2: " & Integer'Image (A2.all)); A2.all := 24; Put_Line ("A1: " & Integer'Image (A1.all)); Put_Line ("A2: " & Integer'Image (A2.all)); end Show_Aliasing;

In this example, we access the object allocated via new by using either A1 or A2, as both refer to the same aliased object. In other words, A1 or A2 allow us to access the same object in memory.

Important

Note that aliasing is unrelated to renaming. For example, we could use renaming to write a program that looks similar to the one above:

    
    
    
        
with Ada.Text_IO; use Ada.Text_IO; procedure Show_Renaming is A1 : Integer; A2 : Integer renames A1; begin A1 := 22; Put_Line ("A1: " & Integer'Image (A1)); Put_Line ("A2: " & Integer'Image (A2)); A2 := 24; Put_Line ("A1: " & Integer'Image (A1)); Put_Line ("A2: " & Integer'Image (A2)); end Show_Renaming;

Here, A1 or A2 are two different names for the same object. However, the object itself isn't aliased.

In the Ada Reference Manual

Aliased objects

As we discussed previously, we use new to create aliased objects on the heap. We can also use general access types to access objects that were created on the stack.

By default, objects created on the stack aren't aliased. Therefore, we have to indicate that an object is aliased by using the aliased keyword in the object's declaration: Obj : aliased Integer;.

Let's see an example:

    
    
    
        
with Ada.Text_IO; use Ada.Text_IO; procedure Show_Aliased_Obj is type Integer_Access is access all Integer; I_Var : aliased Integer; A1 : Integer_Access; begin A1 := I_Var'Access; A1.all := 22; Put_Line ("A1: " & Integer'Image (A1.all)); end Show_Aliased_Obj;

Here, we declare I_Var as an aliased integer variable and get a reference to it, which we assign to A1. Naturally, we could also have two accesses A1 and A2:

    
    
    
        
with Ada.Text_IO; use Ada.Text_IO; procedure Show_Aliased_Obj is type Integer_Access is access all Integer; I_Var : aliased Integer; A1, A2 : Integer_Access; begin A1 := I_Var'Access; A2 := A1; A1.all := 22; Put_Line ("A1: " & Integer'Image (A1.all)); Put_Line ("A2: " & Integer'Image (A2.all)); A2.all := 24; Put_Line ("A1: " & Integer'Image (A1.all)); Put_Line ("A2: " & Integer'Image (A2.all)); end Show_Aliased_Obj;

In this example, both A1 and A2 refer to the I_Var variable.

Note that these examples make use of these two features:

  1. The declaration of a general access type (Integer_Access) using access all.

  2. The retrieval of a reference to I_Var using the Access attribute.

In the next sections, we discuss these features in more details.

In the Ada Reference Manual

General access modifiers

Let's now discuss how to declare general access types. In addition to the standard (pool-specific) access type declarations, Ada provides two access modifiers:

Type

Declaration

Access-to-variable

type T_Acc is access all T

Access-to-constant

type T_Acc is access constant T

Let's look at an example:

    
    
    
        
package Integer_Access_Types is type Integer_Access is access Integer; type Integer_Access_All is access all Integer; type Integer_Access_Const is access constant Integer; end Integer_Access_Types;

As we've seen previously, we can use a type such as Integer_Access to allocate objects dynamically. However, we cannot use this type to refer to declared objects, for example. In this case, we have to use an access-to-variable type such as Integer_Access_All. Also, if we want to access constants — or access objects that we want to treat as constants —, we use a type such as Integer_Access_Const.

Access attribute

To get access to a variable or a constant, we make use of the Access attribute. For example, I_Var'Access gives us access to the I_Var object.

Let's look at an example of how to use the integer access types from the previous code snippet:

    
    
    
        
package Integer_Access_Types is type Integer_Access is access Integer; type Integer_Access_All is access all Integer; type Integer_Access_Const is access constant Integer; procedure Show; end Integer_Access_Types;
with Ada.Text_IO; use Ada.Text_IO; package body Integer_Access_Types is I_Var : aliased Integer := 0; Fact : aliased constant Integer := 42; Dyn_Ptr : constant Integer_Access := new Integer'(30); I_Var_Ptr : constant Integer_Access_All := I_Var'Access; I_Var_C_Ptr : constant Integer_Access_Const := I_Var'Access; Fact_Ptr : constant Integer_Access_Const := Fact'Access; procedure Show is begin Put_Line ("Dyn_Ptr: " & Integer'Image (Dyn_Ptr.all)); Put_Line ("I_Var_Ptr: " & Integer'Image (I_Var_Ptr.all)); Put_Line ("I_Var_C_Ptr: " & Integer'Image (I_Var_C_Ptr.all)); Put_Line ("Fact_Ptr: " & Integer'Image (Fact_Ptr.all)); end Show; end Integer_Access_Types;
with Integer_Access_Types; procedure Show_Access_Modifiers is begin Integer_Access_Types.Show; end Show_Access_Modifiers;

In this example, Dyn_Ptr refers to a dynamically allocated object, I_Var_Ptr refers to the I_Var variable, and Fact_Ptr refers to the Fact constant. We get access to the variable and the constant objects by using the Access attribute.

Also, we declare I_Var_C_Ptr as an access-to-constant, but we get access to the I_Var variable. This simply means the object I_Var_C_Ptr refers to is treated as a constant. Therefore, we can write I_Var := 22;, but we cannot write I_Var_C_Ptr.all := 22;.

In the Ada Reference Manual

Non-aliased objects

As mentioned earlier, by default, declared objects — which are allocated on the stack — aren't aliased. Therefore, we cannot get a reference to those objects. For example:

    
    
    
        
with Ada.Text_IO; use Ada.Text_IO; procedure Show_Access_Error is type Integer_Access is access all Integer; I_Var : Integer; A1 : Integer_Access; begin A1 := I_Var'Access; A1.all := 22; Put_Line ("A1: " & Integer'Image (A1.all)); end Show_Access_Error;

In this example, the compiler complains that we cannot get a reference to I_Var because I_Var is not aliased.

Ragged arrays using aliased objects

We can use aliased objects to declare ragged arrays. For example, we can rewrite a previous program using aliased constant objects:

    
    
    
        
package Data_Processing is type Integer_Array is array (Positive range <>) of Integer; private type Integer_Array_Access is access constant Integer_Array; Tab_1 : aliased constant Integer_Array := (1 => 15); Tab_2 : aliased constant Integer_Array := (12, 15, 20); Tab_3 : aliased constant Integer_Array := (12, 15, 20, 20, 25, 30); Table : constant array (1 .. 3) of Integer_Array_Access := (1 => Tab_1'Access, 2 => Tab_2'Access, 3 => Tab_3'Access); end Data_Processing;

Here, instead of allocating the constant arrays dynamically via new, we declare three aliased arrays (Tab_1, Tab_2 and Tab_3) and get a reference to them in the declaration of Table.

Aliased access objects

It's interesting to mention that access objects can be aliased themselves. Consider this example where we declare the Integer_Access_Access type to refer to an access object:

    
    
    
        
with Ada.Text_IO; use Ada.Text_IO; procedure Show_Aliased_Access_Obj is type Integer_Access is access all Integer; type Integer_Access_Access is access all Integer_Access; I_Var : aliased Integer; A : aliased Integer_Access; B : Integer_Access_Access; begin A := I_Var'Access; B := A'Access; B.all.all := 22; Put_Line ("A: " & Integer'Image (A.all)); Put_Line ("B: " & Integer'Image (B.all.all)); end Show_Aliased_Access_Obj;

After the assignments in this example, B refers to A, which in turn refers to I_Var. Note that this code only compiles because we declare A as an aliased (access) object.

Aliased components

Components of an array or a record can be aliased. This allows us to get access to those components:

    
    
    
        
with Ada.Text_IO; use Ada.Text_IO; procedure Show_Aliased_Components is type Integer_Access is access all Integer; type Rec is record I_Var_1 : Integer; I_Var_2 : aliased Integer; end record; type Integer_Array is array (Positive range <>) of aliased Integer; R : Rec := (22, 24); Arr : Integer_Array (1 .. 3) := (others => 42); A : Integer_Access; begin -- A := R.I_Var_1'Access; -- ^ ERROR: cannot access -- non-aliased -- component A := R.I_Var_2'Access; Put_Line ("A: " & Integer'Image (A.all)); A := Arr (2)'Access; Put_Line ("A: " & Integer'Image (A.all)); end Show_Aliased_Components;

In this example, we get access to the I_Var_2 component of record R. (Note that trying to access the I_Var_1 component would gives us a compilation error, as this component is not aliased.) Similarly, we get access to the second component of array Arr.

Declaring components with the aliased keyword allows us to specify that those are accessible via other paths besides the component name. Therefore, the compiler won't store them in registers. This can be essential when doing low-level programming — for example, when accessing memory-mapped registers. In this case, we want to ensure that the compiler uses the memory address we're specifying (instead of assigning registers for those components).

In the Ada Reference Manual

Aliased parameters

In addition to aliased objects and components, we can declare aliased parameters, as we already discussed in an earlier chapter. As we mentioned there, aliased parameters are always passed by reference, independently of the type we're using.

The parameter mode indicates which type we must use for the access type:

Parameter mode

Type

aliased in

Access-to-constant

aliased out

Access-to-variable

aliased in out

Access-to-variable

Using aliased parameters in a subprogram allows us to get access to those parameters in the body of that subprogram. Let's see an example:

    
    
    
        
package Data_Processing is procedure Proc (I : aliased in out Integer); end Data_Processing;
with Ada.Text_IO; use Ada.Text_IO; package body Data_Processing is procedure Show (I : aliased Integer) is -- ^ equivalent to -- "aliased in Integer" type Integer_Constant_Access is access constant Integer; A : constant Integer_Constant_Access := I'Access; begin Put_Line ("Value : I " & Integer'Image (A.all)); end Show; procedure Set_One (I : aliased out Integer) is type Integer_Access is access all Integer; procedure Local_Set_One (A : Integer_Access) is begin A.all := 1; end Local_Set_One; begin Local_Set_One (I'Access); end Set_One; procedure Proc (I : aliased in out Integer) is type Integer_Access is access all Integer; procedure Add_One (A : Integer_Access) is begin A.all := A.all + 1; end Add_One; begin Show (I); Add_One (I'Access); Show (I); end Proc; end Data_Processing;
with Data_Processing; use Data_Processing; procedure Show_Aliased_Param is I : aliased Integer := 22; begin Proc (I); end Show_Aliased_Param;

Here, Proc has an aliased in out parameter. In Proc's body, we declare the Integer_Access type as an access all type. We use the same approach in body of the Set_One procedure, which has an aliased out parameter. Finally, the Show procedure has an aliased in parameter. Therefore, we declare the Integer_Constant_Access as an access constant type.

Note that parameter aliasing has an influence on how arguments are passed to a subprogram when the parameter is of scalar type. When a scalar parameter is declared as aliased, the corresponding argument is passed by reference. For example, if we had declared procedure Show (I : Integer), the argument for I would be passed by value. However, since we're declaring it as aliased Integer, it is passed by reference.

Accessibility Levels and Rules: An Introduction

This section provides an introduction to accessibility levels and accessibility rules. This topic can be very complicated, and by no means do we intend to cover all the details here. (In fact, discussing all the details about accessibility levels and rules could be a long chapter on its own. If you're interested in them, please refer to the Ada Reference Manual.) In any case, the goal of this section is to present the intention behind the accessibility rules and build intuition on how to best use access types in your code.

In the Ada Reference Manual

Lifetime of objects

First, let's talk a bit about lifetime of objects. We assume you understand the concept, so this section is very short.

In very simple terms, the lifetime of an object indicates when an object still has relevant information. For example, if a variable V gets out of scope, we say that its lifetime has ended. From this moment on, V no longer exists.

For example:

    
    
    
        
with Ada.Text_IO; use Ada.Text_IO; procedure Show_Lifetime is I_Var_1 : Integer := 22; begin Inner_Block : declare I_Var_2 : Integer := 42; begin Put_Line ("I_Var_1: " & Integer'Image (I_Var_1)); Put_Line ("I_Var_2: " & Integer'Image (I_Var_2)); -- I_Var_2 will get out of scope -- when the block finishes. end Inner_Block; -- I_Var_2 is now out of scope... Put_Line ("I_Var_1: " & Integer'Image (I_Var_1)); Put_Line ("I_Var_2: " & Integer'Image (I_Var_2)); -- ^^^^^^^ -- ERROR: lifetime of I_Var_2 has ended! end Show_Lifetime;

In this example, we declare I_Var_1 in the Show_Lifetime procedure, and I_Var_2 in its Inner_Block.

This example doesn't compile because we're trying to use I_Var_2 after its lifetime has ended. However, if such a code could compile and run, the last call to Put_Line would potentially display garbage to the user. (In fact, the actual behavior would be undefined.)

Accessibility Levels

In basic terms, accessibility levels are a mechanism to assess the lifetime of objects (as we've just discussed). The starting point is the library level: this is the base level, and no level can be deeper than that. We start "moving" to deeper levels when we use a library in a subprogram or call other subprograms for example.

Suppose we have a procedure Proc that makes use of a package Pkg, and there's a block in the Proc procedure:

package Pkg is

   --  Library level

end Pkg;

with Pkg; use Pkg;

procedure Proc is

   --  One level deeper than
   --  library level

begin

   declare
      --  Two levels deeper than
      --  library level
  begin
      null;
   end;

end Proc;

For this code, we can say that:

  • the specification of Pkg is at library level;

  • the declarative part of Proc is one level deeper than the library level; and

  • the block is two levels deeper than the library level.

(Note that this is still a very simplified overview of accessibility levels. Things start getting more complicated when we use information from Pkg in Proc. Those details will become more clear in the next sections.)

The levels themselves are not visible to the programmer. For example, there's no Access_Level attribute that returns an integer value indicating the level. Also, you cannot write a user message that displays the level at a certain point. In this sense, accessibility levels are assessed relatively to each other: we can only say that a specific operation is at the same or at a deeper level than another one.

Accessibility Rules

The accessibility rules determine whether a specific use of access types or objects is legal (or not). Actually, accessibility rules exist to prevent dangling references, which we discuss later. Also, they are based on the accessibility levels we discussed earlier.

Code example

As mentioned earlier, the accessibility level at a specific point isn't visible to the programmer. However, to illustrate which level we have at each point in the following code example, we use a prefix (L0, L1, and L2) to indicate whether we're at the library level (L0) or at a deeper level.

Let's now look at the complete code example:

    
    
    
        
package Library_Level is type L0_Integer_Access is access all Integer; L0_IA : L0_Integer_Access; L0_Var : aliased Integer; end Library_Level;
with Library_Level; use Library_Level; procedure Show_Library_Level is type L1_Integer_Access is access all Integer; L0_IA_2 : L0_Integer_Access; L1_IA : L1_Integer_Access; L1_Var : aliased Integer; procedure Test is type L2_Integer_Access is access all Integer; L2_IA : L2_Integer_Access; L2_Var : aliased Integer; begin L1_IA := L2_Var'Access; -- ^^^^^^ -- ILLEGAL: L2 object to -- L1 access object L2_IA := L2_Var'Access; -- ^^^^^^ -- LEGAL: L2 object to -- L2 access object end Test; begin L0_IA := new Integer'(22); -- ^^^^^^^^^^^ -- LEGAL: L0 object to -- L0 access object L0_IA_2 := new Integer'(22); -- ^^^^^^^^^^^ -- LEGAL: L0 object to -- L0 access object L0_IA := L1_Var'Access; -- ^^^^^^ -- ILLEGAL: L1 object to -- L0 access object L0_IA_2 := L1_Var'Access; -- ^^^^^^ -- ILLEGAL: L1 object to -- L0 access object L1_IA := L0_Var'Access; -- ^^^^^^ -- LEGAL: L0 object to -- L1 access object L1_IA := L1_Var'Access; -- ^^^^^^ -- LEGAL: L1 object to -- L1 access object L0_IA := L1_IA; -- ^^^^^ -- ILLEGAL: type mismatch L0_IA := L0_Integer_Access (L1_IA); -- ^^^^^^^^^^^^^^^^^ -- ILLEGAL: cannot convert -- L1 access object to -- L0 access object Test; end Show_Library_Level;

In this example, we declare

  • in the Library_Level package: the L0_Integer_Access type, the L0_IA access object, and the L0_Var aliased variable;

  • in the Show_Library_Level procedure: the L1_Integer_Access type, the L0_IA_2 and L1_IA access objects, and the L1_Var aliased variable;

  • in the nested Test procedure: the L2_Integer_Access type, the L2_IA, and the L2_Var aliased variable.

As mentioned earlier, the Ln prefix indicates the level of each type or object. Here, the n value is zero at library level. We then increment the n value each time we refer to a deeper level.

For instance:

  • when we declare the L1_Integer_Access type in the Show_Library_Level procedure, that declaration is one level deeper than the level of the Library_Level package — so it has the L1 prefix.

  • when we declare the L2_Integer_Access type in the Test procedure, that declaration is one level deeper than the level of the Show_Library_Level procedure — so it has the L2 prefix.

Types and Accessibility Levels

It's very important to highlight the fact that:

  • types themselves also have an associated level, and

  • objects have the same accessibility level as their types.

When we declare the L0_IA_2 object in the code example, its accessibility level is at library level because its type (the L0_Integer_Access type) is at library level. Even though this declaration is in the Show_Library_Level procedure — whose declarative part is one level deeper than the library level —, the object itself has the same accessibility level as its type.

Now that we've discussed the accessibility levels of this code example, let's see how the accessibility rules use those levels.

Operations on Access Types

In very simple terms, the accessibility rules say that:

  • operations on access types at the same accessibility level are legal;

  • assigning or converting to a deeper level is legal;

Otherwise, operations targeting objects at a less-deep level are illegal.

For example, L0_IA := new Integer'(22) and L1_IA := L1_Var'Access are legal because we're operating at the same accessibility level. Also, L1_IA := L0_Var'Access is legal because L1_IA is at a deeper level than L0_Var'Access.

However, many operations in the code example are illegal. For instance, L0_IA := L1_Var'Access and L0_IA_2 := L1_Var'Access are illegal because the target objects in the assignment are less deep.

Note that the L0_IA := L1_IA assignment is mainly illegal because the access types don't match. (Of course, in addition to that, assigning L1_Var'Access to L0_IA is also illegal in terms of accessibility rules.)

Conversion between Access Types

The same rules apply to the conversion between access types. In the code example, the L0_Integer_Access (L1_IA) conversion is illegal because the resulting object is less deep. That being said, conversions on the same level are fine:

    
    
    
        
procedure Show_Same_Level_Conversion is type L1_Integer_Access is access all Integer; type L1_B_Integer_Access is access all Integer; L1_IA : L1_Integer_Access; L1_B_IA : L1_B_Integer_Access; L1_Var : aliased Integer; begin L1_IA := L1_Var'Access; L1_B_IA := L1_B_Integer_Access (L1_IA); -- ^^^^^^^^^^^^^^^^^^^ -- LEGAL: conversion from -- L1 access object to -- L1 access object end Show_Same_Level_Conversion;

Here, we're converting from the L1_Integer_Access type to the L1_B_Integer_Access, which are both at the same level.

Accessibility rules on parameters

Note that the accessibility rules also apply to access values as subprogram parameters. For example, compilation fails for this example:

    
    
    
        
package Names is type Name is access all String; type Constant_Name is access constant String; procedure Show (N : Constant_Name); end Names;
with Ada.Text_IO; use Ada.Text_IO; -- with Ada.Characters.Handling; -- use Ada.Characters.Handling; package body Names is procedure Show (N : Constant_Name) is begin -- for I in N'Range loop -- N (I) := To_Lower (N (I)); -- end loop; Put_Line ("Name: " & N.all); end Show; end Names;
with Names; use Names; procedure Show_Names is S : aliased String := "John"; begin Show (S'Access); end Show_Names;

In this case, the S'Access cannot be used as the actual parameter for the N parameter of the Show procedure because it's in a deeper level. If we allocate the string via new, however, the code compiles as expected:

    
    
    
        
with Names; use Names; procedure Show_Names is S : Name := new String'("John"); begin Show (Constant_Name (S)); end Show_Names;

This version of the code works because both object and access object have the same level.

Dangling References

An access value that points to a non-existent object is called a dangling reference. Later on, we'll discuss how dangling references may occur using unchecked deallocation.

Dangling references are created when we have an access value pointing to an object whose lifetime has ended, so it becomes a non-existent object. This could occur, for example, when an access value still points to an object X that has gone out of scope.

As mentioned in the previous section, the accessibility rules of the Ada language ensure that such situations never happen! In fact, whenever possible, the compiler applies those rules to detect potential dangling references at compile time. When this detection isn't possible at compile time, the compiler introduces an accessibility check. If this check fails at runtime, it raises a Program_Error exception — thereby preventing that a dangling reference gets used.

Let's see an example of how dangling references could occur:

    
    
    
        
with Ada.Text_IO; use Ada.Text_IO; procedure Show_Dangling_Reference is type Integer_Access is access all Integer; I_Var_1 : aliased Integer := 22; A1 : Integer_Access; begin A1 := I_Var_1'Access; Put_Line ("A1.all: " & Integer'Image (A1.all)); Put_Line ("Inner_Block will start now!"); Inner_Block : declare -- -- I_Var_2 only exists in Inner_Block -- I_Var_2 : aliased Integer := 42; -- -- A2 only exists in Inner_Block -- A2 : Integer_Access; begin A2 := I_Var_1'Access; Put_Line ("A2.all: " & Integer'Image (A2.all)); A1 := I_Var_2'Access; -- PROBLEM: A1 and Integer_Access type -- have longer lifetime than -- I_Var_2 Put_Line ("A1.all: " & Integer'Image (A1.all)); A2 := I_Var_2'Access; -- PROBLEM: A2 has the same lifetime as -- I_Var_2, but Integer_Access -- type has a longer lifetime. Put_Line ("A2.all: " & Integer'Image (A2.all)); end Inner_Block; Put_Line ("Inner_Block has ended!"); Put_Line ("A1.all: " & Integer'Image (A1.all)); end Show_Dangling_Reference;

Here, we declare the access objects A1 and A2 of Integer_Access type, and the I_Var_1 and I_Var_2 objects. Moreover, A1 and I_Var_1 are declared in the scope of the Show_Dangling_Reference procedure, while A2 and I_Var_2 are declared in the Inner_Block.

When we try to compile this code, we get two compilation errors due to violation of accessibility rules. Let's now discuss these accessibility rules in terms of lifetime, and see which problems they are preventing in each case.

  1. In the A1 := I_Var_2'Access assignment, the main problem is that A1 has a longer lifetime than I_Var_2. After the Inner_Block finishes — when I_Var_2 gets out of scope and its lifetime has ended —, A1 would still be pointing to an object that does not longer exist.

  2. In the A2 := I_Var_2'Access assignment, however, both A2 and I_Var_2 have the same lifetime. In that sense, the assignment may actually look pretty much OK.

    • However, as mentioned in the previous section, Ada also cares about the lifetime of access types. In fact, since the Integer_Access type is declared outside of the Inner_Block, it has a longer lifetime than A2 and I_Var_2.

    • To be more precise, the accessibility rules detect that A2 is an access object of a type that has a longer lifetime than I_Var_2.

At first glance, this last accessibility rule may seem too strict, as both A2 and I_Var_2 have the same lifetime — so nothing bad could occur when dereferencing A2. However, consider the following change to the code:

A2 := I_Var_2'Access;

A1 := A2;
--    PROBLEM: A1 will still be referring
--             to I_Var_2 after the
--             Inner_Block, i.e. when the
--             lifetime of I_Var_2 has
--             ended!

Here, we're introducing the A1 := A2 assignment. The problem with this is that I_Var_2's lifetime ends when the Inner_Block finishes, but A1 would continue to refer to an I_Var_2 object that doesn't exist anymore — thereby creating a dangling reference.

Even though we're actually not assigning A2 to A1 in the original code, we could have done it. The accessibility rules ensure that such an error is never introduced into the program.

For further reading...

In the original code, we can consider the A2 := I_Var_2'Access assignment to be safe, as we're not using the A1 := A2 assignment there. Since we're confident that no error could ever occur in the Inner_Block due to the assignment to A2, we could replace it with A2 := I_Var_2'Unchecked_Access, so that the compiler accepts it. We discuss more about the unchecked access attribute later in this chapter.

Alternatively, we could have solved the compilation issue that we see in the A2 := I_Var_2'Access assignment by declaring another access type locally in the Inner_Block:

Inner_Block : declare
   type Integer_Local_Access is
     access all Integer;

   I_Var_2 : aliased Integer := 42;

   A2      : Integer_Local_Access;
begin
   A2 := I_Var_2'Access;
   --   This assignment is fine because
   --   the Integer_Local_Access type has
   --   the same lifetime as I_Var_2.
end Inner_Block;

With this change, A2 becomes an access object of a type that has the same lifetime as I_Var_2, so that the assignment doesn't violate the rules anymore.

(Note that in the Inner_Block, we could have simply named the local access type Integer_Access instead of Integer_Local_Access, thereby masking the Integer_Access type of the outer block.)

We discuss the effects of dereferencing dangling references later in this chapter.

Unchecked Access

In this section, we discuss the Unchecked_Access attribute, which we can use to circumvent accessibility issues for objects in specific cases. (Note that this attribute only exists for objects, not for subprograms.)

We've seen previously that the accessibility levels verify the lifetime of access types. Let's see a simplified version of a code example from that section:

    
    
    
        
package Integers is type Integer_Access is access all Integer; end Integers;
with Ada.Text_IO; use Ada.Text_IO; with Integers; use Integers; procedure Show_Access_Issue is I_Var : aliased Integer := 42; A : Integer_Access; begin A := I_Var'Access; -- PROBLEM: A has the same lifetime as I_Var, -- but Integer_Access type has a -- longer lifetime. Put_Line ("A.all: " & Integer'Image (A.all)); end Show_Access_Issue;

Here, the compiler complains about the A := I_Var'Access assignment because the Integer_Access type has a longer lifetime than A. However, we know that this assignment to A — and further uses of A in the code — won't cause dangling references to be created. Therefore, we can assume that assigning the access to I_Var to A is safe.

When we're sure that an access assignment cannot possibly generate dangling references, we can the use Unchecked_Access attribute. For instance, we can use this attribute to circumvent the compilation error in the previous code example, since we know that the assignment is actually safe:

    
    
    
        
package Integers is type Integer_Access is access all Integer; end Integers;
with Ada.Text_IO; use Ada.Text_IO; with Integers; use Integers; procedure Show_Access_Issue is I_Var : aliased Integer := 42; A : Integer_Access; begin A := I_Var'Unchecked_Access; -- OK: assignment is now accepted. Put_Line ("A.all: " & Integer'Image (A.all)); end Show_Access_Issue;

When we use the Unchecked_Access attribute, most rules still apply. The only difference to the standard Access attribute is that unchecked access applies the rules as if the object we're getting access to was being declared at library level. (For the code example we've just seen, the check would be performed as if I_Var was declared in the Integers package instead of being declared in the procedure.)

It is strongly recommended to avoid unchecked access in general. You should only use it when you can safely assume that the access object will be discarded before the object we had access to gets out of scope. Therefore, if this situation isn't clear enough, it's best to avoid unchecked access. (Later in this chapter, we'll see some of the nasty issues that arrive from creating dangling references.) Instead, you should work on improving the software design of your application by considering alternatives such as using containers or encapsulating access types in well-designed abstract data types.

In the Ada Reference Manual

Unchecked Deallocation

So far, we've seen multiple examples of using new to allocate objects. In this section, we discuss how to manually deallocate objects.

Our starting point to manually deallocate an object is the generic Ada.Unchecked_Deallocation procedure. We first instantiate this procedure for an access type whose objects we want to be able to deallocate. For example, let's instantiate it for the Integer_Access type:

    
    
    
        
with Ada.Unchecked_Deallocation; package Integer_Types is type Integer_Access is access Integer; -- -- Instantiation of Ada.Unchecked_Deallocation -- for the Integer_Access type: -- procedure Free is new Ada.Unchecked_Deallocation (Object => Integer, Name => Integer_Access); end Integer_Types;

Here, we declare the Free procedure, which we can then use to deallocate objects that were allocated for the Integer_Access type.

Ada.Unchecked_Deallocation is a generic procedure that we can instantiate for access types. When declaring an instance of Ada.Unchecked_Deallocation, we have to specify arguments for:

  • the formal Object parameter, which indicates the type of actual objects that we want to deallocate; and

  • the formal Name parameter, which indicates the access type.

In a type declaration such as type Integer_Access is access Integer, Integer denotes the Object, while Integer_Access denotes the Name.

Because each instance of Ada.Unchecked_Deallocation is bound to a specific access type, we cannot use it for another access type, even if the type we use for the Object parameter is the same:

    
    
    
        
with Ada.Unchecked_Deallocation; package Integer_Types is type Integer_Access is access Integer; procedure Free is new Ada.Unchecked_Deallocation (Object => Integer, Name => Integer_Access); type Another_Integer_Access is access Integer; procedure Free is new Ada.Unchecked_Deallocation (Object => Integer, Name => Another_Integer_Access); end Integer_Types;

Here, we're declaring two Free procedures: one for the Integer_Access type, another for the Another_Integer_Access. We cannot use the Free procedure for the Integer_Access type when deallocating objects associated with the Another_Integer_Access type, even though both types are declared as access Integer.

Note that we can use any name when instantiating the Ada.Unchecked_Deallocation procedure. However, naming it Free is very common.

Now, let's see a complete example that includes object allocation and deallocation:

    
    
    
        
with Ada.Unchecked_Deallocation; package Integer_Types is type Integer_Access is access Integer; procedure Free is new Ada.Unchecked_Deallocation (Object => Integer, Name => Integer_Access); procedure Show_Is_Null (I : Integer_Access); end Integer_Types;
with Ada.Text_IO; use Ada.Text_IO; package body Integer_Types is procedure Show_Is_Null (I : Integer_Access) is begin if I = null then Put_Line ("access value is null."); else Put_Line ("access value is NOT null."); end if; end Show_Is_Null; end Integer_Types;
with Ada.Text_IO; use Ada.Text_IO; with Integer_Types; use Integer_Types; procedure Show_Unchecked_Deallocation is I : Integer_Access; begin Put ("We haven't called new yet... "); Show_Is_Null (I); Put ("Calling new... "); I := new Integer; Show_Is_Null (I); Put ("Calling Free... "); Free (I); Show_Is_Null (I); end Show_Unchecked_Deallocation;

In the Show_Unchecked_Deallocation procedure, we first allocate an object for I and then call Free (I) to deallocate it. Also, we call the Show_Is_Null procedure at three different points: before any allocation takes place, after allocating an object for I, and after deallocating that object.

When we deallocate an object via a call to Free, the corresponding access value — which was previously pointing to an existing object — is set to null. Therefore, I = null after the call to Free, which is exactly what we see when running this example code.

Note that it is OK to call Free multiple times for the same access object:

    
    
    
        
with Integer_Types; use Integer_Types; procedure Show_Unchecked_Deallocation is I : Integer_Access; begin I := new Integer; Free (I); Free (I); Free (I); end Show_Unchecked_Deallocation;

The multiple calls to Free for the same access object don't cause any issues. Because the access value is null after the first call to Free (I), we're actually just passing null as an argument in the second and third calls to Free. However, any attempt to deallocate an access value of null is ignored in the Free procedure, so the second and third calls to Free don't have any effect.

Unchecked Deallocation and Dangling References

We've discussed dangling references before. In this section, we discuss how unchecked deallocation can create dangling references and the issues of having them in an application.

Let's reuse the last example and introduce I_2, which will point to the same object as I:

    
    
    
        
with Integer_Types; use Integer_Types; procedure Show_Unchecked_Deallocation is I, I_2 : Integer_Access; begin I := new Integer; I_2 := I; -- NOTE: I_2 points to the same -- object as I. -- -- Use I and I_2... -- -- ... then deallocate memory... -- Free (I); -- NOTE: at this point, I_2 is a -- dangling reference! -- Further calls to Free (I) -- are OK! Free (I); Free (I); -- A call to Free (I_2) is -- NOT OK: Free (I_2); end Show_Unchecked_Deallocation;

As we've seen before, we can have multiple calls to Free (I). However, the call to Free (I_2) is bad because I_2 is not null. In fact, it is a dangling reference — i.e. I_2 points to an object that doesn't exist anymore. Also, the first call to Free (I) will reclaim the storage that was allocated for the object that I originally referred to. The call to Free (I_2) will then try to reclaim the previously-reclaimed object, but it'll fail in an undefined manner.

Because of these potential errors, you should be very careful when using unchecked deallocation: it is the programmer's responsibility to avoid creating dangling references!

For the example we've just seen, we could avoid creating a dangling reference by explicitly assigning null to I_2 to indicate that it doesn't point to any specific object:

    
    
    
        
with Integer_Types; use Integer_Types; procedure Show_Unchecked_Deallocation is I, I_2 : Integer_Access; begin I := new Integer; I_2 := I; -- NOTE: I_2 points to the same -- object as I. -- -- Use I and I_2... -- -- ... then deallocate memory... -- I_2 := null; -- NOTE: now, I_2 doesn't point to -- any object, so calling -- Free (I_2) is OK. Free (I); Free (I_2); end Show_Unchecked_Deallocation;

Now, calling Free (I_2) doesn't cause any issues because it doesn't point to any object.

Note, however, that this code example is just meant to illustrate the issues of dangling pointers and how we could circumvent them. We're not suggesting to use this approach when designing an implementation. In fact, it's not practical for the programmer to make every possible dangling reference become null if the calls to Free are strewn throughout the code.

The suggested design is to not use Free in the client code, but instead hide its use within bigger abstractions. In that way, all the occurrences of the calls to Free are in one package, and the programmer of that package can then prevent dangling references. We'll discuss these design strategies later on.

Dereferencing dangling references

Of course, you shouldn't try to dereference a dangling reference because your program becomes erroneous, as we discuss in this section. Let's see an example:

    
    
    
        
with Ada.Text_IO; use Ada.Text_IO; with Integer_Types; use Integer_Types; procedure Show_Unchecked_Deallocation is I_1, I_2 : Integer_Access; begin I_1 := new Integer'(42); I_2 := I_1; Put_Line ("I_1.all = " & Integer'Image (I_1.all)); Put_Line ("I_2.all = " & Integer'Image (I_2.all)); Put_Line ("Freeing I_1"); Free (I_1); if I_1 /= null then Put_Line ("I_1.all = " & Integer'Image (I_1.all)); end if; if I_2 /= null then Put_Line ("I_2.all = " & Integer'Image (I_2.all)); end if; end Show_Unchecked_Deallocation;

In this example, we allocate an object for I_1 and make I_2 point to the same object. Then, we call Free (I), which has the following consequences:

  • The call to Free (I_1) will try to reclaim the storage for the original object (I_1.all), so it may be reused for other allocations.

  • I_1 = null after the call to Free (I_1).

  • I_2 becomes a dangling reference by the call to Free (I_1).

    • In other words, I_2 is still non-null, and what it points to is now undefined.

In principle, we could check for null before trying to dereference the access value. (Remember that when deallocating an object via a call to Free, the corresponding access value is set to null.) In fact, this strategy works fine for I_1, but it doesn't work for I_2 because the access value is not null. As a consequence, the application tries to dereference I_2.

Dereferencing a dangling reference is erroneous: the behavior is undefined in this case. For the example we've just seen,

  • I_2.all might make the application crash;

  • I_2.all might give us a different value than before;

  • I_2.all might even give us the same value as before (42) if the original object is still available.

Because the effect is unpredictable, it might be really difficult to debug the application and identify the cause.

Having dangling pointers in an application should be avoided at all costs! Again, it is the programmer's responsibility to be very careful when using unchecked deallocation: avoid creating dangling references!

Restrictions for Ada.Unchecked_Deallocation

There are two unsurprising restrictions for Ada.Unchecked_Deallocation:

  1. It cannot be instantiated for access-to-constant types; and

  2. It cannot be used when the Storage_Size aspect of a type is zero (i.e. when its storage pool is empty).

(Note that this last restriction also applies to the allocation via new.)

Let's see an example of these restrictions:

    
    
    
        
with Ada.Unchecked_Deallocation; procedure Show_Unchecked_Deallocation_Errors is type Integer_Access_Zero is access Integer with Storage_Size => 0; procedure Free is new Ada.Unchecked_Deallocation (Object => Integer, Name => Integer_Access_Zero); type Constant_Integer_Access is access constant Integer; -- ERROR: Cannot use access-to-constant type -- for Name procedure Free is new Ada.Unchecked_Deallocation (Object => Integer, Name => Constant_Integer_Access); I : Integer_Access_Zero; begin -- ERROR: Cannot allocate objects from -- empty storage pool I := new Integer; -- ERROR: Cannot deallocate objects from -- empty storage pool Free (I); end Show_Unchecked_Deallocation_Errors;

Here, we see that trying to instantiate Ada.Unchecked_Deallocation for the Constant_Integer_Access type is rejected by the compiler. Similarly, we cannot allocate or deallocate an object for the Integer_Access_Zero type because its storage pool is empty.

Null & Not Null Access

Note

This section was originally written by Robert A. Duff and published as Gem #23: Null Considered Harmful and Gem #24.

Ada, like many languages, defines a special null value for access types. All values of an access type designate some object of the designated type, except for null, which does not designate any object. The null value can be used as a special flag. For example, a singly-linked list can be null-terminated. A Lookup function can return null to mean "not found", presuming the result is of an access type:

    
    
    
        
package Show_Null_Return is type Ref_Element is access all Element; Not_Found : constant Ref_Element := null; function Lookup (T : Table) return Ref_Element; -- Returns Not_Found if not found. end Show_Null_Return;

An alternative design for Lookup would be to raise an exception:

    
    
    
        
package Show_Not_Found_Exception is Not_Found : exception; function Lookup (T : Table) return Ref_Element; -- Raises Not_Found if not found. -- Never returns null. end Show_Not_Found_Exception;

Neither design is better in all situations; it depends in part on whether we consider the "not found" situation to be exceptional.

Clearly, the client calling Lookup needs to know whether it can return null, and if so, what that means. In general, it's a good idea to document whether things can be null or not, especially for formal parameters and function results. Prior to Ada 2005, we would do that with comments. Since Ada 2005, we can use the not null syntax:

    
    
    
        
package Show_Not_Null_Return is type Ref_Element is access all Element; Not_Found : constant Ref_Element := null; function Lookup (T : Table) return not null Ref_Element; -- Possible since Ada 2005. end Show_Not_Null_Return;

This is a complete package for the code snippets above:

    
    
    
        
package Example is type Element is limited private; type Ref_Element is access all Element; type Table is limited private; Not_Found : constant Ref_Element := null; function Lookup (T : Table) return Ref_Element; -- Returns Not_Found if not found. Not_Found_2 : exception; function Lookup_2 (T : Table) return not null Ref_Element; -- Raises Not_Found_2 if not found. procedure P (X : not null Ref_Element); procedure Q (X : not null Ref_Element); private type Element is limited record Component : Integer; end record; type Table is limited null record; end Example;
package body Example is An_Element : aliased Element; function Lookup (T : Table) return Ref_Element is pragma Unreferenced (T); begin -- ... return Not_Found; end Lookup; function Lookup_2 (T : Table) return not null Ref_Element is begin -- ... raise Not_Found_2; return An_Element'Access; -- suppress error: 'missing "return" -- statement in function body' end Lookup_2; procedure P (X : not null Ref_Element) is begin X.all.Component := X.all.Component + 1; end P; procedure Q (X : not null Ref_Element) is begin for I in 1 .. 1000 loop P (X); end loop; end Q; procedure R is begin Q (An_Element'Access); end R; pragma Unreferenced (R); end Example;

In general, it's better to use the language proper for documentation, when possible, rather than comments, because compile-time and/or run-time checks can help ensure that the "documentation" is actually true. With comments, there's a greater danger that the comment will become false during maintenance, and false documentation is obviously a menace.

In many, perhaps most cases, null is just a tripping hazard. It's a good idea to put in not null when possible. In fact, a good argument can be made that not null should be the default, with extra syntax required when null is wanted. This is the way Standard ML works, for example — you don't get any special null-like value unless you ask for it. Of course, because Ada 2005 needs to be compatible with previous versions of the language, not null cannot be the default for Ada.

One word of caution: access objects are default-initialized to null, so if you have a not null object (or component) you had better initialize it explicitly, or you will get Constraint_Error. not null is more often useful on parameters and function results, for this reason.

Another advantage of not null over comments is for efficiency. Consider procedures P and Q in this example:

    
    
    
        
package Example.Processing is procedure P (X : not null Ref_Element); procedure Q (X : not null Ref_Element); end Example.Processing;
package body Example.Processing is procedure P (X : not null Ref_Element) is begin X.all.Component := X.all.Component + 1; end P; procedure Q (X : not null Ref_Element) is begin for I in 1 .. 1000 loop P (X); end loop; end Q; end Example.Processing;

Without not null, the generated code for P will do a check that X /= null, which may be costly on some systems. P is called in a loop, so this check will likely occur many times. With not null, the check is pushed to the call site. Pushing checks to the call site is usually beneficial because

  1. the check might be hoisted out of a loop by the optimizer, or

  2. the check might be eliminated altogether, as in the example above, where the compiler knows that An_Element'Access cannot be null.

This is analogous to the situation with other run-time checks, such as array bounds checks:

    
    
    
        
package Show_Process_Array is type My_Index is range 1 .. 10; type My_Array is array (My_Index) of Integer; procedure Process_Array (X : in out My_Array; Index : My_Index); end Show_Process_Array;
package body Show_Process_Array is procedure Process_Array (X : in out My_Array; Index : My_Index) is begin X (Index) := X (Index) + 1; end Process_Array; end Show_Process_Array;

If X (Index) occurs inside Process_Array, there is no need to check that Index is in range, because the check is pushed to the caller.

Design strategies for access types

Previously, we learned about dangling references and discussed the effects of dereferencing them. Also, we've seen the relationship between unchecked deallocation and dangling references. Ensuring that all calls to Free for a specific access type will never cause dangling references can become an arduous task — if not impossible — if those calls are located in different parts of the source code.

Although we used access types directly in the main application in many of the previous code examples from this chapter, this approach was in fact selected just for illustration purposes — i.e. to make the code look simpler. In general, however, we should avoid this approach. Instead, our recommendation is to encapsulate the access types in some form of abstraction. In this section, we discuss design strategies for access types that take this recommendation into account.

Abstract data type for access types

The simplest form of abstraction is of course an abstract data type. For example, we could declare a limited private type, which allows us to hide the access type and to avoid copies of references that could potentially become dangling references. (We discuss limited private types later in another chapter.)

Let's see an example:

    
    
    
        
package Access_Type_Abstraction is type Info is limited private; function To_Info (S : String) return Info; function To_String (Obj : Info) return String; function Copy (Obj : Info) return Info; procedure Copy (To : in out Info; From : Info); procedure Append (Obj : in out Info; S : String); procedure Reset (Obj : in out Info); procedure Destroy (Obj : in out Info); private type Info is access String; end Access_Type_Abstraction;
with Ada.Unchecked_Deallocation; package body Access_Type_Abstraction is function To_Info (S : String) return Info is (new String'(S)); function To_String (Obj : Info) return String is (if Obj /= null then Obj.all else ""); function Copy (Obj : Info) return Info is (To_Info (Obj.all)); procedure Copy (To : in out Info; From : Info) is begin Destroy (To); To := To_Info (From.all); end Copy; procedure Append (Obj : in out Info; S : String) is New_Info : constant Info := To_Info (To_String (Obj) & S); begin Destroy (Obj); Obj := New_Info; end Append; procedure Reset (Obj : in out Info) is begin Destroy (Obj); end Reset; procedure Destroy (Obj : in out Info) is procedure Free is new Ada.Unchecked_Deallocation (Object => String, Name => Info); begin Free (Obj); end Destroy; end Access_Type_Abstraction;
with Ada.Text_IO; use Ada.Text_IO; with Access_Type_Abstraction; use Access_Type_Abstraction; procedure Main is Obj_1 : Info := To_Info ("hello"); Obj_2 : Info := Copy (Obj_1); begin Put_Line ("TO_INFO / COPY"); Put_Line ("Obj_1 : " & To_String (Obj_1)); Put_Line ("Obj_2 : " & To_String (Obj_2)); Put_Line ("----------"); Reset (Obj_1); Append (Obj_2, " world"); Put_Line ("RESET / APPEND"); Put_Line ("Obj_1 : " & To_String (Obj_1)); Put_Line ("Obj_2 : " & To_String (Obj_2)); Put_Line ("----------"); Copy (From => Obj_2, To => Obj_1); Put_Line ("COPY"); Put_Line ("Obj_1 : " & To_String (Obj_1)); Put_Line ("Obj_2 : " & To_String (Obj_2)); Put_Line ("----------"); Destroy (Obj_1); Destroy (Obj_2); Put_Line ("DESTROY"); Put_Line ("Obj_1 : " & To_String (Obj_1)); Put_Line ("Obj_2 : " & To_String (Obj_2)); Put_Line ("----------"); Append (Obj_1, "hey"); Put_Line ("APPEND"); Put_Line ("Obj_1 : " & To_String (Obj_1)); Put_Line ("----------"); Put_Line ("APPEND"); Append (Obj_1, " there"); Put_Line ("Obj_1 : " & To_String (Obj_1)); Destroy (Obj_1); Destroy (Obj_2); end Main;

In this example, we hide an access type in the Info type — a limited private type. We allocate an object of this type in the To_Info function and deallocate it in the Destroy procedure. Also, we make sure that the reference isn't copied in the Copy function — we only copy the designated value in this function. This strategy eliminates the possibility of dangling references, as each reference is encapsulated in an object of Info type.

Controlled type for access types

In the previous code example, the Destroy procedure had to be called to deallocate the hidden access object. We could make sure that this deallocation happens automatically by using a controlled (or limited controlled) type. (We discuss controlled types in another chapter.)

Let's adapt the previous example and declare Info as a limited controlled type:

    
    
    
        
with Ada.Finalization; package Access_Type_Abstraction is type Info is limited private; function To_Info (S : String) return Info; function To_String (Obj : Info) return String; function Copy (Obj : Info) return Info; procedure Copy (To : in out Info; From : Info); procedure Append (Obj : in out Info; S : String); procedure Reset (Obj : in out Info); private type String_Access is access String; type Info is new Ada.Finalization.Limited_Controlled with record Str_A : String_Access; end record; procedure Initialize (Obj : in out Info); procedure Finalize (Obj : in out Info); end Access_Type_Abstraction;
with Ada.Unchecked_Deallocation; package body Access_Type_Abstraction is -- -- STRING_ACCESS SUBPROGRAMS -- function To_String_Access (S : String) return String_Access is (new String'(S)); function To_String (S : String_Access) return String is (if S /= null then S.all else ""); procedure Free is new Ada.Unchecked_Deallocation (Object => String, Name => String_Access); procedure Copy (To : in out String_Access; From : String_Access) is begin Free (To); To := To_String_Access (From.all); end Copy; procedure Append (Obj : in out String_Access; S : String) is New_Str : constant String_Access := To_String_Access (To_String (Obj) & S); begin Free (Obj); Obj := New_Str; end Append; -- -- PRIVATE SUBPROGRAMS -- procedure Initialize (Obj : in out Info) is begin -- Put_Line ("Initializing Info"); Obj.Str_A := null; -- ^^^^^^^^^^^^^ -- NOTE: This line has just been added to -- illustrate the "automatic" call to -- Initialize. Actually, this -- assignment isn't needed, as -- the Str_A component is -- automatically initialized to null -- upon object construction. end Initialize; procedure Finalize (Obj : in out Info) is begin -- Put_Line ("Finalizing Info"); Free (Obj.Str_A); end Finalize; -- -- PUBLIC SUBPROGRAMS -- function To_Info (S : String) return Info is (Ada.Finalization.Limited_Controlled with Str_A => To_String_Access (S)); function To_String (Obj : Info) return String is (To_String (Obj.Str_A)); function Copy (Obj : Info) return Info is (To_Info (To_String (Obj.Str_A))); procedure Copy (To : in out Info; From : Info) is begin Copy (To.Str_A, From.Str_A); end Copy; procedure Append (Obj : in out Info; S : String) is begin Append (Obj.Str_A, S); end Append; procedure Reset (Obj : in out Info) is begin Free (Obj.Str_A); end Reset; end Access_Type_Abstraction;
with Ada.Text_IO; use Ada.Text_IO; with Access_Type_Abstraction; use Access_Type_Abstraction; procedure Main is Obj_1 : Info := To_Info ("hello"); Obj_2 : Info := Copy (Obj_1); begin Put_Line ("TO_INFO / COPY"); Put_Line ("Obj_1 : " & To_String (Obj_1)); Put_Line ("Obj_2 : " & To_String (Obj_2)); Put_Line ("----------"); Reset (Obj_1); Append (Obj_2, " world"); Put_Line ("RESET / APPEND"); Put_Line ("Obj_1 : " & To_String (Obj_1)); Put_Line ("Obj_2 : " & To_String (Obj_2)); Put_Line ("----------"); Copy (From => Obj_2, To => Obj_1); Put_Line ("COPY"); Put_Line ("Obj_1 : " & To_String (Obj_1)); Put_Line ("Obj_2 : " & To_String (Obj_2)); Put_Line ("----------"); Reset (Obj_1); Reset (Obj_2); Put_Line ("RESET"); Put_Line ("Obj_1 : " & To_String (Obj_1)); Put_Line ("Obj_2 : " & To_String (Obj_2)); Put_Line ("----------"); Append (Obj_1, "hey"); Put_Line ("APPEND"); Put_Line ("Obj_1 : " & To_String (Obj_1)); Put_Line ("----------"); Put_Line ("APPEND"); Append (Obj_1, " there"); Put_Line ("Obj_1 : " & To_String (Obj_1)); end Main;

Of course, because we're using the Limited_Controlled type from the Ada.Finalization package, we had to adapt the prototype of the subprograms from the Access_Type_Abstraction. In this version of the code, we only have the allocation taking place in the To_Info procedure, but we don't have a Destroy procedure for deallocation: this call was moved to the Finalize procedure.

Since objects of the Info type — such as Obj_1 in the Show_Access_Type_Abstraction procedure — are now controlled, the Finalize procedure is automatically called when they go out of scope. In this procedure, which we override for the Info type, we perform the deallocation of the internal access object Str_A. (You may uncomment the calls to Put_Line in the body of the Initialize and Finalize subprograms to confirm that these subprograms are called in the background.)

Access to subprograms

So far in this chapter, we focused mainly on access-to-objects. However, we can use access types to subprograms. This is the topic of this section.

Static vs. dynamic calls

In a typical subprogram call, we indicate the subprogram we want to call statically. For example, let's say we've implemented a procedure Proc that calls a procedure P:

    
    
    
        
procedure P (I : in out Integer);
procedure P (I : in out Integer) is begin null; end P;
with P; procedure Proc is I : Integer := 0; begin P (I); end Proc;

The call to P is statically dispatched: every time Proc runs and calls P, that call is always to the same procedure. In other words, we can determine at compilation time which procedure is called.

In contrast, an access to a subprogram allows us to dynamically indicate which subprogram we want to call. For example, if we change Proc in the code above to receive the access to a subprogram P as a parameter, the actual procedure that would be called when running Proc would be determined at run time, and it might be different for every call to Proc. In this case, we wouldn't be able to determine at compilation time which procedure would be called in every case. (In some cases, however, it could still be possible to determine which procedure is called by analyzing the argument that is passed to Proc.)

Access to subprogram declaration

We declare an access to a subprogram as a type by writing access procedure or access function and the corresponding prototype:

    
    
    
        
package Access_To_Subprogram_Types is type Access_To_Procedure is access procedure (I : in out Integer); type Access_To_Function is access function (I : Integer) return Integer; end Access_To_Subprogram_Types;

In the designated profile of the access type declarations, we list all the parameters that we expect in the subprogram.

We can use those types to declare access to subprograms — as subprogram parameters, for example:

    
    
    
        
with Access_To_Subprogram_Types; use Access_To_Subprogram_Types; package Access_To_Subprogram_Params is procedure Proc (P : Access_To_Procedure); end Access_To_Subprogram_Params;
package body Access_To_Subprogram_Params is procedure Proc (P : Access_To_Procedure) is I : Integer := 0; begin P (I); -- P.all (I); end Proc; end Access_To_Subprogram_Params;

In the implementation of the Proc procedure of the code example, we call the P procedure by simply passing I as a parameter. In this case, P is automatically dereferenced. We may, however, explicitly dereference P by writing P.all (I).

Before we use this package, let's implement a simple procedure that we'll use later on:

    
    
    
        
procedure Add_Ten (I : in out Integer);
procedure Add_Ten (I : in out Integer) is begin I := I + 10; end Add_Ten;

Now, we can get access to a subprogram by using the Access attribute and pass it as an actual parameter:

    
    
    
        
with Access_To_Subprogram_Params; use Access_To_Subprogram_Params; with Add_Ten; procedure Show_Access_To_Subprograms is begin Proc (Add_Ten'Access); -- ^ Getting access to Add_Ten -- procedure and passing it -- to Proc end Show_Access_To_Subprograms;

Here, we get access to the Add_Ten procedure and pass it to the Proc procedure.

In the Ada Reference Manual

Objects of access-to-subprogram type

In the previous example, the Proc procedure had a parameter of access-to-subprogram type. In addition to parameters, we can of course declare objects of access-to-subprogram types as well. For example, we can extend our previous test application and declare an object P of access-to-subprogram type. Before we do so, however, let's implement another small procedure that we'll use later on:

    
    
    
        
procedure Add_Twenty (I : in out Integer);
procedure Add_Twenty (I : in out Integer) is begin I := I + 20; end Add_Twenty;

In addition to Add_Ten, we've implemented the Add_Twenty procedure, which we use in our extended test application:

    
    
    
        
with Access_To_Subprogram_Types; use Access_To_Subprogram_Types; with Access_To_Subprogram_Params; use Access_To_Subprogram_Params; with Add_Ten; with Add_Twenty; procedure Show_Access_To_Subprograms is P : Access_To_Procedure; Some_Int : Integer := 0; begin P := Add_Ten'Access; -- ^ Getting access to Add_Ten -- procedure and assigning it -- to P Proc (P); -- ^ Passing access-to-subprogram as an -- actual parameter P (Some_Int); -- ^ Using access-to-subprogram object in a -- subprogram call P := Add_Twenty'Access; -- ^ Getting access to Add_Twenty -- procedure and assigning it -- to P Proc (P); P (Some_Int); end Show_Access_To_Subprograms;

In the Show_Access_To_Subprograms procedure, we see the declaration of our access-to-subprogram object P (of Access_To_Procedure type). We get access to the Add_Ten procedure and assign it to P, and we then do the same for the Add_Twenty procedure.

We can use an access-to-subprogram object either as the actual parameter of a subprogram call, or in a subprogram call. In the code example, we're passing P as the actual parameter of the Proc procedure in the Proc (P) calls. Also, we're calling the subprogram assigned to (designated by the current value of) P in the P (Some_Int) calls.

Components of access-to-subprogram type

In addition to declaring subprogram parameters and objects of access-to-subprogram types, we can declare components of these types. For example:

    
    
    
        
package Access_To_Subprogram_Types is type Access_To_Procedure is access procedure (I : in out Integer); type Access_To_Function is access function (I : Integer) return Integer; type Access_To_Procedure_Array is array (Positive range <>) of Access_To_Procedure; type Access_To_Function_Array is array (Positive range <>) of Access_To_Function; type Rec_Access_To_Procedure is record AP : Access_To_Procedure; end record; type Rec_Access_To_Function is record AF : Access_To_Function; end record; end Access_To_Subprogram_Types;

Here, the access-to-procedure type Access_To_Procedure is used as a component of the array type Access_To_Procedure_Array and the record type Rec_Access_To_Procedure. Similarly, the access-to-function type Access_To_Function type is used as a component of the array type Access_To_Function_Array and the record type Rec_Access_To_Function.

Let's see two test applications using these types. First, let's use the Access_To_Procedure_Array array type in a test application:

    
    
    
        
with Ada.Text_IO; use Ada.Text_IO; with Access_To_Subprogram_Types; use Access_To_Subprogram_Types; with Add_Ten; with Add_Twenty; procedure Show_Access_To_Subprograms is PA : constant Access_To_Procedure_Array (1 .. 2) := (Add_Ten'Access, Add_Twenty'Access); Some_Int : Integer := 0; begin Put_Line ("Some_Int: " & Some_Int'Image); for I in PA'Range loop PA (I) (Some_Int); Put_Line ("Some_Int: " & Some_Int'Image); end loop; end Show_Access_To_Subprograms;

Here, we declare the PA array and use the access to the Add_Ten and Add_Twenty procedures as its components. We can call any of these procedures by simply specifying the index of the component, e.g. PA (2). Once we specify the procedure we want to use, we simply pass the parameters, e.g.: PA (2) (Some_Int).

Now, let's use the Rec_Access_To_Procedure record type in a test application:

    
    
    
        
with Ada.Text_IO; use Ada.Text_IO; with Access_To_Subprogram_Types; use Access_To_Subprogram_Types; with Add_Ten; with Add_Twenty; procedure Show_Access_To_Subprograms is RA : Rec_Access_To_Procedure; Some_Int : Integer := 0; begin Put_Line ("Some_Int: " & Some_Int'Image); RA := (AP => Add_Ten'Access); RA.AP (Some_Int); Put_Line ("Some_Int: " & Some_Int'Image); RA := (AP => Add_Twenty'Access); RA.AP (Some_Int); Put_Line ("Some_Int: " & Some_Int'Image); end Show_Access_To_Subprograms;

Here, we declare two record aggregates where we specify the AP component, e.g.: (AP => Add_Ten'Access), which indicates the access-to-subprogram we want to use. We can call the subprogram by simply accessing the AP component, i.e.: RA.AP.

Access-to-subprogram as discriminant types

As you might expect, we can use access-to-subprogram types when declaring discriminants. In fact, when we were talking about discriminants as access values earlier on, we used access-to-object types in our code examples, but we could have used access-to-subprogram types as well. For example:

    
    
    
        
package Custom_Processing is -- Declaring an access type: type Integer_Processing is access procedure (I : in out Integer); -- Declaring a discriminant with this -- access type: type Rec (IP : Integer_Processing) is private; procedure Init (R : in out Rec; Value : Integer); procedure Process (R : in out Rec); procedure Show (R : Rec); private type Rec (IP : Integer_Processing) is record I : Integer := 0; end record; end Custom_Processing;
with Ada.Text_IO; use Ada.Text_IO; package body Custom_Processing is procedure Init (R : in out Rec; Value : Integer) is begin R.I := Value; end Init; procedure Process (R : in out Rec) is begin R.IP (R.I); -- ^^^^^^ -- Calling procedure that we specified as -- the record's discriminant end Process; procedure Show (R : Rec) is begin Put_Line ("R.I = " & Integer'Image (R.I)); end Show; end Custom_Processing;

In this example, we declare the access-to-subprogram type Integer_Processing, which we use as the IP discriminant of the Rec type. In the Process procedure, we call the IP procedure that we specified as the record's discriminant (R.IP (R.I)).

Before we look at a test application for this package, let's implement another small procedure:

    
    
    
        
procedure Mult_Two (I : in out Integer);
procedure Mult_Two (I : in out Integer) is begin I := I * 2; end Mult_Two;

Now, let's look at the test application:

    
    
    
        
with Ada.Text_IO; use Ada.Text_IO; with Custom_Processing; use Custom_Processing; with Add_Ten; with Mult_Two; procedure Show_Access_To_Subprogram_Discriminants is R_Add_Ten : Rec (IP => Add_Ten'Access); -- ^^^^^^^^^^^^^^^^^^^^ -- Using access-to-subprogram as a -- discriminant R_Mult_Two : Rec (IP => Mult_Two'Access); -- ^^^^^^^^^^^^^^^^^^^^^ -- Using access-to-subprogram as a -- discriminant begin Init (R_Add_Ten, 1); Init (R_Mult_Two, 2); Put_Line ("---- R_Add_Ten ----"); Show (R_Add_Ten); Put_Line ("Calling Process procedure..."); Process (R_Add_Ten); Show (R_Add_Ten); Put_Line ("---- R_Mult_Two ----"); Show (R_Mult_Two); Put_Line ("Calling Process procedure..."); Process (R_Mult_Two); Show (R_Mult_Two); end Show_Access_To_Subprogram_Discriminants;

In this procedure, we declare the R_Add_Ten and R_Mult_Two of Rec type and specify the access to Add_Ten and Mult_Two, respectively, as the IP discriminant. The procedure we specified here is then called inside a call to the Process procedure.

Access-to-subprograms as formal parameters

We can use access-to-subprograms types when declaring formal parameters. For example, let's revisit the Custom_Processing package from the previous section and convert it into a generic package.

    
    
    
        
generic type T is private; -- -- Declaring formal access-to-subprogram -- type: -- type T_Processing is access procedure (Element : in out T); -- -- Declaring formal access-to-subprogram -- parameter: -- Proc : T_Processing; with function Image_T (Element : T) return String; package Gen_Custom_Processing is type Rec is private; procedure Init (R : in out Rec; Value : T); procedure Process (R : in out Rec); procedure Show (R : Rec); private type Rec is record Comp : T; end record; end Gen_Custom_Processing;
with Ada.Text_IO; use Ada.Text_IO; package body Gen_Custom_Processing is procedure Init (R : in out Rec; Value : T) is begin R.Comp := Value; end Init; procedure Process (R : in out Rec) is begin Proc (R.Comp); end Process; procedure Show (R : Rec) is begin Put_Line ("R.Comp = " & Image_T (R.Comp)); end Show; end Gen_Custom_Processing;

In this version of the procedure, instead of declaring Proc as a discriminant of the Rec record, we're declaring it as a formal parameter of the Gen_Custom_Processing package. Also, we're declaring an access-to-subprogram type (T_Processing) as a formal parameter. (Note that, in contrast to these two parameters that we've just mentioned, Image_T is not a formal access-to-subprogram parameter: it's actually just a formal subprogram.)

We then instantiate the Gen_Custom_Processing package in our test application:

    
    
    
        
with Gen_Custom_Processing; with Add_Ten; with Ada.Text_IO; use Ada.Text_IO; procedure Show_Access_To_Subprogram_As_Formal_Parameter is type Integer_Processing is access procedure (I : in out Integer); package Custom_Processing is new Gen_Custom_Processing (T => Integer, T_Processing => Integer_Processing, -- ^^^^^^^^^^^^^^^^^^ -- access-to-subprogram type Proc => Add_Ten'Access, -- ^^^^^^^^^^^^^^^^^^ -- access-to-subprogram Image_T => Integer'Image); use Custom_Processing; R_Add_Ten : Rec; begin Init (R_Add_Ten, 1); Put_Line ("---- R_Add_Ten ----"); Show (R_Add_Ten); Put_Line ("Calling Process procedure..."); Process (R_Add_Ten); Show (R_Add_Ten); end Show_Access_To_Subprogram_As_Formal_Parameter;

Here, we instantiate the Gen_Custom_Processing package as Custom_Processing and specify the access-to-subprogram type and the access-to-subprogram.

Selecting subprograms

A practical application of access to subprograms is that it enables us to dynamically select a subprogram and pass it to another subprogram, where it can then be called.

For example, we may have a Process procedure that receives a logging procedure as a parameter (Log_Proc). Also, this parameter may be null by default — so that no procedure is called if the parameter isn't specified:

    
    
    
        
package Data_Processing is type Data_Container is array (Positive range <>) of Float; type Log_Procedure is access procedure (D : Data_Container); procedure Process (D : in out Data_Container; Log_Proc : Log_Procedure := null); end Data_Processing;
package body Data_Processing is procedure Process (D : in out Data_Container; Log_Proc : Log_Procedure := null) is begin -- missing processing part... if Log_Proc /= null then Log_Proc (D); end if; end Process; end Data_Processing;

In the implementation of Process, we check whether Log_Proc is null or not. (If it's not null, we call the procedure. Otherwise, we just skip the call.)

Now, let's implement two logging procedures that match the expected form of the Log_Procedure type:

    
    
    
        
with Ada.Text_IO; use Ada.Text_IO; with Data_Processing; use Data_Processing; procedure Log_Element_Per_Line (D : Data_Container) is begin Put_Line ("Elements: "); for V of D loop Put_Line (V'Image); end loop; Put_Line ("------"); end Log_Element_Per_Line;
with Ada.Text_IO; use Ada.Text_IO; with Data_Processing; use Data_Processing; procedure Log_Csv (D : Data_Container) is begin for I in D'First .. D'Last - 1 loop Put (D (I)'Image & ", "); end loop; Put (D (D'Last)'Image); New_Line; end Log_Csv;

Finally, we implement a test application that selects each of the logging procedures that we've just implemented:

    
    
    
        
with Ada.Text_IO; use Ada.Text_IO; with Data_Processing; use Data_Processing; with Log_Element_Per_Line; with Log_Csv; procedure Show_Access_To_Subprograms is D : Data_Container (1 .. 5) := (others => 1.0); begin Put_Line ("==== Log_Element_Per_Line ===="); Process (D, Log_Element_Per_Line'Access); Put_Line ("==== Log_Csv ===="); Process (D, Log_Csv'Access); Put_Line ("==== None ===="); Process (D); end Show_Access_To_Subprograms;

Here, we use the Access attribute to get access to the Log_Element_Per_Line and Log_Csv procedures. Also, in the third call, we don't pass any access as an argument, which is then null by default.

Null exclusion

We can use null exclusion when declaring an access to subprograms. By doing so, we ensure that a subprogram must be specified — either as a parameter or when initializing an access object. Otherwise, an exception is raised. Let's adapt the previous example and introduce the Init_Function type:

    
    
    
        
package Data_Processing is type Data_Container is array (Positive range <>) of Float; type Init_Function is not null access function return Float; procedure Process (D : in out Data_Container; Init_Func : Init_Function); end Data_Processing;
package body Data_Processing is procedure Process (D : in out Data_Container; Init_Func : Init_Function) is begin for I in D'Range loop D (I) := Init_Func.all; end loop; end Process; end Data_Processing;

In this case, we specify that Init_Function is not null access because we want to always be able to call this function in the Process procedure (i.e. without raising an exception).

When an access to a subprogram doesn't have parameters — which is the case for the subprograms of Init_Function type — we need to explicitly dereference it by writing .all. (In this case, .all isn't optional.) Therefore, we have to write Init_Func.all in the implementation of the Process procedure of the code example.

Now, let's declare two simple functions — Init_Zero and Init_One — that return 0.0 and 1.0, respectively:

    
    
    
        
function Init_Zero return Float;
function Init_One return Float;
function Init_Zero return Float is begin return 0.0; end Init_Zero;
function Init_One return Float is begin return 1.0; end Init_One;

Finally, let's see a test application where we select each of the init functions we've just implemented:

    
    
    
        
with Ada.Text_IO; use Ada.Text_IO; with Data_Processing; use Data_Processing; procedure Log_Element_Per_Line (D : Data_Container) is begin Put_Line ("Elements: "); for V of D loop Put_Line (V'Image); end loop; Put_Line ("------"); end Log_Element_Per_Line;
with Ada.Text_IO; use Ada.Text_IO; with Data_Processing; use Data_Processing; with Init_Zero; with Init_One; with Log_Element_Per_Line; procedure Show_Access_To_Subprograms is D : Data_Container (1 .. 5) := (others => 1.0); begin Put_Line ("==== Init_Zero ===="); Process (D, Init_Zero'Access); Log_Element_Per_Line (D); Put_Line ("==== Init_One ===="); Process (D, Init_One'Access); Log_Element_Per_Line (D); -- Put_Line ("==== None ===="); -- Process (D, null); -- Log_Element_Per_Line (D); end Show_Access_To_Subprograms;

Here, we use the Access attribute to get access to the Init_Zero and Init_One functions. Also, if we uncomment the call to Process with null as an argument for the init function, we see that the Constraint_Error exception is raised at run time — as the argument cannot be null due to the null exclusion.

For further reading...

Note

This example was originally written by Robert A. Duff and was part of the Gem #24.

Here's another example, first with null:

    
    
    
        
package Show_Null_Procedure is type Element is limited null record; -- Not implemented yet type Ref_Element is access all Element; type Table is limited null record; -- Not implemented yet type Iterate_Action is access procedure (X : not null Ref_Element); procedure Iterate (T : Table; Action : Iterate_Action := null); -- If Action is null, do nothing. end Show_Null_Procedure;

and without null:

    
    
    
        
package Show_Null_Procedure is type Element is limited null record; -- Not implemented yet type Ref_Element is access all Element; type Table is limited null record; -- Not implemented yet procedure Do_Nothing (X : not null Ref_Element) is null; type Iterate_Action is access procedure (X : not null Ref_Element); procedure Iterate (T : Table; Action : not null Iterate_Action := Do_Nothing'Access); end Show_Null_Procedure;

The style of the second Iterate is clearly better because it makes use of the syntax to indicate that a procedure is expected. This is a complete package that includes both versions of the Iterate procedure:

    
    
    
        
package Example is type Element is limited private; type Ref_Element is access all Element; type Table is limited private; type Iterate_Action is access procedure (X : not null Ref_Element); procedure Iterate (T : Table; Action : Iterate_Action := null); -- If Action is null, do nothing. procedure Do_Nothing (X : not null Ref_Element) is null; procedure Iterate_2 (T : Table; Action : not null Iterate_Action := Do_Nothing'Access); private type Element is limited record Component : Integer; end record; type Table is limited null record; end Example;
package body Example is An_Element : aliased Element; procedure Iterate (T : Table; Action : Iterate_Action := null) is begin if Action /= null then Action (An_Element'Access); -- In a real program, this would do -- something more sensible. end if; end Iterate; procedure Iterate_2 (T : Table; Action : not null Iterate_Action := Do_Nothing'Access) is begin Action (An_Element'Access); -- In a real program, this would do -- something more sensible. end Iterate_2; end Example;
with Example; use Example; procedure Show_Example is T : Table; begin Iterate_2 (T); end Show_Example;

Writing not null Iterate_Action might look a bit more complicated, but it's worthwhile, and anyway, as mentioned earlier, the compatibility requirement requires that the not null be explicit, rather than the other way around.

Access to protected subprograms

Up to this point, we've discussed access to normal Ada subprograms. In some situations, however, we might want to have access to protected subprograms. To do this, we can simply declare a type using access protected:

    
    
    
        
package Simple_Protected_Access is type Access_Proc is access protected procedure; protected Obj is procedure Do_Something; end Obj; Acc : Access_Proc := Obj.Do_Something'Access; end Simple_Protected_Access;
package body Simple_Protected_Access is protected body Obj is procedure Do_Something is begin -- Not doing anything -- for the moment... null; end Do_Something; end Obj; end Simple_Protected_Access;

Here, we declare the Access_Proc type as an access type to protected procedures. Then, we declare the variable Acc and assign to it the access to the Do_Something procedure (of the protected object Obj).

Now, let's discuss a more useful example: a simple system that allows us to register protected procedures and execute them. This is implemented in Work_Registry package:

    
    
    
        
package Work_Registry is type Work_Id is tagged limited private; type Work_Handler is access protected procedure (T : Work_Id); subtype Valid_Work_Handler is not null Work_Handler; type Work_Handlers is array (Positive range <>) of Work_Handler; protected type Work_Handler_Registry (Last : Positive) is procedure Register (T : Valid_Work_Handler); procedure Reset; procedure Process_All; private D : Work_Handlers (1 .. Last); Curr : Natural := 0; end Work_Handler_Registry; private type Work_Id is tagged limited null record; end Work_Registry;
package body Work_Registry is protected body Work_Handler_Registry is procedure Register (T : Valid_Work_Handler) is begin if Curr < Last then Curr := Curr + 1; D (Curr) := T; end if; end Register; procedure Reset is begin Curr := 0; end Reset; procedure Process_All is Dummy_ID : Work_Id; begin for I in D'First .. Curr loop D (I).all (Dummy_ID); end loop; end Process_All; end Work_Handler_Registry; end Work_Registry;

Here, we declare the protected Work_Handler_Registry type with the following subprograms:

  • Register, which we can use to register a protected procedure;

  • Reset, which we can use to reset the system; and

  • Process_All, which we can use to call all procedures that were registered in the system.

Work_Handler is our access to protected subprogram type. Also, we declare the Valid_Work_Handler subtype, which excludes null. By doing so, we can ensure that only valid procedures are passed to the Register procedure. In the protected Work_Handler_Registry type, we store the procedures in an array (of Work_Handlers type).

Important

Note that, in the type declaration Work_Handler, we say that the protected procedure must have a parameter of Work_Id type. In this example, this parameter is just used to bind the procedure to the Work_Handler_Registry type. The Work_Id type itself is actually declared as a null record (in the private part of the package), and it isn't really useful on its own.

If we had declared type Work_Handler is access protected procedure; instead, we would be able to register any protected procedure into the system, even the ones that might not be suitable for the system. By using a parameter of Work_Id type, however, we make use of strong typing to ensure that only procedures that were designed for the system can be registered.

In the next part of the code, we declare the Integer_Storage type, which is a simple protected type that we use to store an integer value:

    
    
    
        
with Work_Registry; package Integer_Storage_System is protected type Integer_Storage is procedure Set (V : Integer); procedure Show (T : Work_Registry.Work_Id); private I : Integer := 0; end Integer_Storage; type Integer_Storage_Access is access Integer_Storage; type Integer_Storage_Array is array (Positive range <>) of Integer_Storage_Access; end Integer_Storage_System;
with Ada.Text_IO; use Ada.Text_IO; package body Integer_Storage_System is protected body Integer_Storage is procedure Set (V : Integer) is begin I := V; end Set; procedure Show (T : Work_Registry.Work_Id) is pragma Unreferenced (T); begin Put_Line ("Value: " & Integer'Image (I)); end Show; end Integer_Storage; end Integer_Storage_System;

For the Integer_Storage type, we declare two procedures:

  • Set, which we use to assign a value to the (protected) integer value; and

  • Show, which we use to show the integer value that is stored in the protected object.

The Show procedure has a parameter of Work_Id type, which indicates that this procedure was designed to be registered in the system of Work_Handler_Registry type.

Finally, we have a test application in which we declare a registry (WHR) and an array of "protected integer objects" (Int_Stor):

    
    
    
        
with Work_Registry; use Work_Registry; with Integer_Storage_System; use Integer_Storage_System; procedure Show_Access_To_Protected_Subprograms is WHR : Work_Handler_Registry (5); Int_Stor : Integer_Storage_Array (1 .. 3); begin -- Allocate and initialize integer storage -- -- (For the initialization, we're just -- assigning the index here, but we could -- really have used any integer value.) for I in Int_Stor'Range loop Int_Stor (I) := new Integer_Storage; Int_Stor (I).Set (I); end loop; -- Register handlers for I in Int_Stor'Range loop WHR.Register (Int_Stor (I).all.Show'Access); end loop; -- Now, use Process_All to call the handlers -- (in this case, the Show procedure for -- each protected object from Int_Stor). WHR.Process_All; end Show_Access_To_Protected_Subprograms;

The work handler registry (WHR) has a maximum capacity of five procedures, whereas the Int_Stor array has a capacity of three elements. By calling WHR.Register and passing Int_Stor (I).all.Show'Access, we register the Show procedure of each protected object from Int_Stor.

Important

Note that the components of the Int_Stor array are of Integer_Storage_Access type, which is declared as an access to Integer_Storage objects. Therefore, we have to dereference the object (by writing Int_Stor (I).all) before getting access to the Show procedure (by writing .Show'Access).

We have to use an access type here because we cannot pass the access (to the Show procedure) of a local object in the call to the Register procedure. Therefore, the protected objects (of Integer_Storage type) cannot be local.

This issue becomes evident if we replace the declaration of Int_Stor with a local array (and then adapt the remaining code). If we do this, we get a compilation error in the call to Register:

    
    
    
        
with Work_Registry; use Work_Registry; with Integer_Storage_System; use Integer_Storage_System; procedure Show_Access_To_Protected_Subprograms is WHR : Work_Handler_Registry (5); Int_Stor : array (1 .. 3) of Integer_Storage; begin -- Allocate and initialize integer storage -- -- (For the initialization, we're just -- assigning the index here, but we could -- really have used any integer value.) for I in Int_Stor'Range loop -- Int_Stor (I) := new Integer_Storage; Int_Stor (I).Set (I); end loop; -- Register handlers for I in Int_Stor'Range loop WHR.Register (Int_Stor (I).Show'Access); -- ^ ERROR! end loop; -- Now, call the handlers -- (i.e. the Show procedure of each -- protected object). WHR.Process_All; end Show_Access_To_Protected_Subprograms;

As we've just discussed, this error is due to the fact that Int_Stor is now a "local" protected object, and the accessibility rules don't allow mixing it with non-local accesses in order to prevent the possibility of dangling references.

When we call WHR.Process_All, the registry system calls each procedure that has been registered with the system. When looking at the values displayed by the test application, we may notice that each call to Show is referring to a different protected object. In fact, even though we're passing just the access to a protected procedure in the call to Register, that access is also associated to a specific protected object. (This is different from access to non-protected subprograms we've discussed previously: in that case, there's no object associated.) If we replace the argument to Register by Int_Stor (2).all.Show'Access, for example, the three Show procedures registered in the system will now refer to the same protected object (stored at Int_Stor (2)).

Also, even though we have registered the same procedure (Show) of the same type (Integer_Storage) in all calls to Register, we could have used a different protected procedure — and of a different protected type. As an exercise, we could, for example, create a new type called Float_Storage (based on the code that we used for the Integer_Storage type) and register some objects of Float_Storage type into the system (with a couple of additional calls to Register). If we then call WHR.Process_All, we'd see that the system is able to cope with objects of both Integer_Storage and Float_Storage types. In fact, the system implemented with the Work_Handler_Registry can be seen as "type agnostic," as it doesn't care about which type the protected objects have — as long as the subprograms we want to register are conformant to the Valid_Work_Handler type.

Accessibility Rules and Access-To-Subprograms

In general, the accessibility rules that we discussed previously for access-to-objects also apply to access-to-subprograms. In this section, we discuss minor differences when applying those rules to access-to-subprograms.

In our discussion about accessibility rules, we've looked into accessibility levels and the accessibility rules that are based on those levels. The same accessibility rules apply to access-to-subprograms. As we said previously, operations targeting objects at a less-deep level are illegal, as it's the case for subprograms as well:

    
    
    
        
package Access_To_Subprogram_Types is type Access_To_Procedure is access procedure (I : in out Integer); type Access_To_Function is access function (I : Integer) return Integer; end Access_To_Subprogram_Types;
with Ada.Text_IO; use Ada.Text_IO; with Access_To_Subprogram_Types; use Access_To_Subprogram_Types; procedure Show_Access_To_Subprogram_Error is Func : Access_To_Function; Value : Integer := 0; begin declare function Add_One (I : Integer) return Integer is (I + 1); begin Func := Add_One'Access; -- This assignment is illegal because the -- Access_To_Function type is less deep -- than Add_One. end; Put_Line ("Value: " & Value'Image); Value := Func (Value); Put_Line ("Value: " & Value'Image); end Show_Access_To_Subprogram_Error;

Obviously, we can correct this error by putting the Add_One function at the same level as the Access_To_Function type, i.e. at library level:

    
    
    
        
package Access_To_Subprogram_Types is type Access_To_Procedure is access procedure (I : in out Integer); type Access_To_Function is access function (I : Integer) return Integer; end Access_To_Subprogram_Types;
function Add_One (I : Integer) return Integer;
function Add_One (I : Integer) return Integer is begin return I + 1; end Add_One;
with Ada.Text_IO; use Ada.Text_IO; with Access_To_Subprogram_Types; use Access_To_Subprogram_Types; with Add_One; procedure Show_Access_To_Subprogram_Error is Func : Access_To_Function; Value : Integer := 0; begin Func := Add_One'Access; Put_Line ("Value: " & Value'Image); Value := Func (Value); Put_Line ("Value: " & Value'Image); end Show_Access_To_Subprogram_Error;

As a recommendation, resolving accessibility issues in the case of access-to-subprograms is best done by refactoring the subprograms of your source code — for example, moving subprograms to a different level.

Unchecked Access

Previously, we discussed about the Unchecked_Access attribute, which we can use to circumvent accessibility issues in specific cases for access-to-objects. We also said in that section that this attribute only exists for objects, not for subprograms. We can use the previous example to illustrate this limitation:

    
    
    
        
package Access_To_Subprogram_Types is type Access_To_Procedure is access procedure (I : in out Integer); type Access_To_Function is access function (I : Integer) return Integer; end Access_To_Subprogram_Types;
with Ada.Text_IO; use Ada.Text_IO; with Access_To_Subprogram_Types; use Access_To_Subprogram_Types; procedure Show_Access_To_Subprogram_Error is Func : Access_To_Function; function Add_One (I : Integer) return Integer is (I + 1); Value : Integer := 0; begin Func := Add_One'Access; Put_Line ("Value: " & Value'Image); Value := Func (Value); Put_Line ("Value: " & Value'Image); end Show_Access_To_Subprogram_Error;

When we analyze the Show_Access_To_Subprogram_Error procedure, we see that the Func object and the Add_One function have the same lifetime. Therefore, in this very specific case, we could safely assign Add_One'Access to Func and call Func for Value. Due to the accessibility rules, however, this assignment is illegal. (Obviously, the accessibility issue here is that the Access_To_Function type has a potentially longer lifetime.)

In the case of access-to-objects, we could use Unchecked_Access to enforce assignments that we consider safe after careful analysis. However, because this attribute isn't available for access-to-subprograms, the best solution is to move the subprogram to a level that allows the assignment to be legal, as we said before.

In the GNAT toolchain

GNAT offers an equivalent for Unchecked_Access that can be used for subprograms: the Unrestricted_Access attribute. Note, however, that this attribute is not portable.

    
    
    
        
package Access_To_Subprogram_Types is type Access_To_Procedure is access procedure (I : in out Integer); type Access_To_Function is access function (I : Integer) return Integer; end Access_To_Subprogram_Types;
with Ada.Text_IO; use Ada.Text_IO; with Access_To_Subprogram_Types; use Access_To_Subprogram_Types; procedure Show_Access_To_Subprogram_Error is Func : Access_To_Function; function Add_One (I : Integer) return Integer is (I + 1); Value : Integer := 0; begin Func := Add_One'Unrestricted_Access; -- ^^^^^^^^^^^^^^^^^^^ -- Allowing access to local function Put_Line ("Value: " & Value'Image); Value := Func (Value); Put_Line ("Value: " & Value'Image); end Show_Access_To_Subprogram_Error;

As we can see, the Unrestricted_Access attribute can be safely used in this specific case to circumvent the accessibility rule limitation.

Access and Address

As we know, an access type is not a pointer, and it doesn't just indicate an address in memory. In fact, to represent an address in Ada, we use the Address type. Also, as we discussed earlier, we can use operators such as <, >, + and - for addresses. In contrast to that, those operators aren't available for access types — except, of course, for = and /=.

In certain situations, however, we might need to convert between access types and addresses. In this section, we discuss how to do so.

Address and access conversion

The generic System.Address_To_Access_Conversions package allows us to convert between access types and addresses. This might be useful for specific low-level operations. Let's see an example:

    
    
    
        
with Ada.Text_IO; use Ada.Text_IO; with System.Address_To_Access_Conversions; with System.Address_Image; procedure Show_Address_Conversion is package Integer_AAC is new System.Address_To_Access_Conversions (Object => Integer); use Integer_AAC; subtype Integer_Access is Integer_AAC.Object_Pointer; -- This is similar to: -- -- type Integer_Access is access all Integer; I : aliased Integer := 5; AI : Integer_Access := I'Access; begin Put_Line ("I'Address : " & System.Address_Image (I'Address)); Put_Line ("AI.all'Address : " & System.Address_Image (AI.all'Address)); Put_Line ("To_Address (AI) : " & System.Address_Image (To_Address (AI))); end Show_Address_Conversion;

In this example, we instantiate the generic System.Address_To_Access_Conversions package using Integer as our target object type. This new package (Integer_AAC) has an Object_Pointer type, which is equivalent to a declaration such as type Integer_Access is access all Integer. (In this example, we declare Integer_Access as a subtype of Integer_AAC.Object_Pointer to illustrate that.)

The Integer_AAC package also includes the To_Address function, which converts an access object to an address. If the actual parameter is not null, To_Address returns the same information as if we were using the Address attribute for the designated object. In other words, To_Address (AI) = AI.all'Address when AI /= null.

If the access value is null, To_Address returns Null_Address, while .all'Address makes the access check fail because we have to dereference the access object (via .all) before retrieving its address (via the Address attribute).

In addition to the To_Address function, the To_Pointer function is available to convert from an address to an object of access type. For example:

    
    
    
        
with Ada.Text_IO; use Ada.Text_IO; with System; use System; with System.Address_To_Access_Conversions; with System.Address_Image; procedure Show_Address_Conversion is package Integer_AAC is new System.Address_To_Access_Conversions (Object => Integer); use Integer_AAC; subtype Integer_Access is Integer_AAC.Object_Pointer; I : aliased Integer := 5; AI_1, AI_2 : Integer_Access; A : Address; begin AI_1 := I'Access; A := To_Address (AI_1); AI_2 := To_Pointer (A); Put_Line ("AI_1.all'Address : " & System.Address_Image (AI_1.all'Address)); Put_Line ("AI_2.all'Address : " & System.Address_Image (AI_2.all'Address)); if AI_1 = AI_2 then Put_Line ("AI_1 = AI_2"); else Put_Line ("AI_1 /= AI_2"); end if; end Show_Address_Conversion;

Here, we convert the A address back to an access value by calling To_Pointer (A). (When running this object, we see that AI_1 and AI_2 have the same access value.)

Conversion of unbounded designated types

Note that the conversions might not work in all cases. For instance, when the designated type — indicated by the formal Object parameter of the generic Address_To_Access_Conversions package — is unbounded, the result of a call to To_Pointer may not have bounds.

Let's adapt the previous code example and replace the Integer type by the (unbounded) String type:

    
    
    
        
with Ada.Text_IO; use Ada.Text_IO; with System; use System; with System.Address_To_Access_Conversions; with System.Address_Image; procedure Show_Address_Conversion is package String_AAC is new System.Address_To_Access_Conversions (Object => String); use String_AAC; subtype Integer_Access is String_AAC.Object_Pointer; S : aliased String := "Hello"; AI_1, AI_2 : Integer_Access; A : Address; begin AI_1 := S'Access; A := To_Address (AI_1); AI_2 := To_Pointer (A); -- ^^^^^^^^^^^^^^ -- WARNING: Result might not have bounds Put_Line ("AI_1.all'Address : " & System.Address_Image (AI_1.all'Address)); Put_Line ("AI_2.all'Address : " & System.Address_Image (AI_2.all'Address)); if AI_1 = AI_2 then Put_Line ("AI_1 = AI_2"); else Put_Line ("AI_1 /= AI_2"); end if; Put_Line ("AI_1: " & AI_1.all); Put_Line ("AI_2: " & AI_2.all); -- ^^^^^^^^^^ -- WARNING: As AI_2 might not have bounds -- due to the call to To_Pointer -- the behavior of this call to -- the "&" operator is -- unpredictable. end Show_Address_Conversion;

In this case, the call to To_Pointer (A) might not have bounds, so any operation on AI_2 might lead to unpredictable results.