Arrays

Arrays provide another fundamental family of composite types in Ada.

Array type declaration

Arrays in Ada are used to define contiguous collections of elements that can be selected by indexing. Here's a simple example:

    
    
    
        
with Ada.Text_IO; use Ada.Text_IO; procedure Greet is type My_Int is range 0 .. 1000; type Index is range 1 .. 5; type My_Int_Array is array (Index) of My_Int; -- ^ Type of elements -- ^ Bounds of the array Arr : My_Int_Array := (2, 3, 5, 7, 11); -- ^ Array literal -- (aggregate) V : My_Int; begin for I in Index loop V := Arr (I); -- ^ Take the Ith element Put (My_Int'Image (V)); end loop; New_Line; end Greet;

The first point to note is that we specify the index type for the array, rather than its size. Here we declared an integer type named Index ranging from 1 to 5, so each array instance will have 5 elements, with the initial element at index 1 and the last element at index 5.

Although this example used an integer type for the index, Ada is more general: any discrete type is permitted to index an array, including Enum types. We will soon see what that means.

Another point to note is that querying an element of the array at a given index uses the same syntax as for function calls: that is, the array object followed by the index in parentheses.

Thus when you see an expression such as A (B), whether it is a function call or an array subscript depends on what A refers to.

Finally, notice how we initialize the array with the (2, 3, 5, 7, 11) expression. This is another kind of aggregate in Ada, and is in a sense a literal expression for an array, in the same way that 3 is a literal expression for an integer. The notation is very powerful, with a number of properties that we will introduce later. A detailed overview appears in the notation of aggregate types.

Unrelated to arrays, the example also illustrated two procedures from Ada.Text_IO:

  • Put, which displays a string without a terminating end of line

  • New_Line, which outputs an end of line

Let's now delve into what it means to be able to use any discrete type to index into the array.

In other languages

Semantically, an array object in Ada is the entire data structure, and not simply a handle or pointer. Unlike C and C++, there is no implicit equivalence between an array and a pointer to its initial element.

    
    
    
        
with Ada.Text_IO; use Ada.Text_IO; procedure Array_Bounds_Example is type My_Int is range 0 .. 1000; type Index is range 11 .. 15; -- ^ Low bound can -- be any value type My_Int_Array is array (Index) of My_Int; Tab : constant My_Int_Array := (2, 3, 5, 7, 11); begin for I in Index loop Put (My_Int'Image (Tab (I))); end loop; New_Line; end Array_Bounds_Example;

One effect is that the bounds of an array can be any values. In the first example we constructed an array type whose first index is 1, but in the example above we declare an array type whose first index is 11.

That's perfectly fine in Ada, and moreover since we use the index type as a range to iterate over the array indices, the code using the array does not need to change.

That leads us to an important consequence with regard to code dealing with arrays. Since the bounds can vary, you should not assume / hard-code specific bounds when iterating / using arrays. That means the code above is good, because it uses the index type, but a for loop as shown below is bad practice even though it works correctly:

for I in 11 .. 15 loop
   Tab (I) := Tab (I) * 2;
end loop;

Since you can use any discrete type to index an array, enumeration types are permitted.

    
    
    
        
with Ada.Text_IO; use Ada.Text_IO; procedure Month_Example is type Month_Duration is range 1 .. 31; type Month is (Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec); type My_Int_Array is array (Month) of Month_Duration; -- ^ Can use an enumeration type -- as the index Tab : constant My_Int_Array := -- ^ constant is like a variable but -- cannot be modified (31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31); -- Maps months to number of days -- (ignoring leap years) Feb_Days : Month_Duration := Tab (Feb); -- Number of days in February begin for M in Month loop Put_Line (Month'Image (M) & " has " & Month_Duration'Image (Tab (M)) & " days."); -- ^ Concatenation operator end loop; end Month_Example;

In the example above, we are:

  • Creating an array type mapping months to month durations in days.

  • Creating an array, and instantiating it with an aggregate mapping months to their actual durations in days.

  • Iterating over the array, printing out the months, and the number of days for each.

Being able to use enumeration values as indices is very helpful in creating mappings such as shown above one, and is an often used feature in Ada.

Indexing

We have already seen the syntax for selecting elements of an array. There are however a few more points to note.

First, as is true in general in Ada, the indexing operation is strongly typed. If you use a value of the wrong type to index the array, you will get a compile-time error.

    
    
    
        
with Ada.Text_IO; use Ada.Text_IO; procedure Greet is type My_Int is range 0 .. 1000; type My_Index is range 1 .. 5; type Your_Index is range 1 .. 5; type My_Int_Array is array (My_Index) of My_Int; Tab : My_Int_Array := (2, 3, 5, 7, 11); begin for I in Your_Index loop Put (My_Int'Image (Tab (I))); -- ^ Compile time error end loop; New_Line; end Greet;

Second, arrays in Ada are bounds checked. This means that if you try to access an element outside of the bounds of the array, you will get a run-time error instead of accessing random memory as in unsafe languages.

    
    
    
        
with Ada.Text_IO; use Ada.Text_IO; procedure Greet is type My_Int is range 0 .. 1000; type Index is range 1 .. 5; type My_Int_Array is array (Index) of My_Int; Tab : My_Int_Array := (2, 3, 5, 7, 11); begin for I in Index range 2 .. 6 loop Put (My_Int'Image (Tab (I))); -- ^ Will raise an -- exception when -- I = 6 end loop; New_Line; end Greet;

Simpler array declarations

In the previous examples, we have always explicitly created an index type for the array. While this can be useful for typing and readability purposes, sometimes you simply want to express a range of values. Ada allows you to do that, too.

    
    
    
        
with Ada.Text_IO; use Ada.Text_IO; procedure Simple_Array_Bounds is type My_Int is range 0 .. 1000; type My_Int_Array is array (1 .. 5) of My_Int; -- ^ Subtype of Integer Tab : constant My_Int_Array := (2, 3, 5, 7, 11); begin for I in 1 .. 5 loop -- ^ Subtype of Integer Put (My_Int'Image (Tab (I))); end loop; New_Line; end Simple_Array_Bounds;

This example defines the range of the array via the range syntax, which specifies an anonymous subtype of Integer and uses it to index the array.

This means that the type of the index is Integer. Similarly, when you use an anonymous range in a for loop as in the example above, the type of the iteration variable is also Integer, so you can use I to index Tab.

You can also use a named subtype for the bounds for an array.

Range attribute

We noted earlier that hard coding bounds when iterating over an array is a bad idea, and showed how to use the array's index type/subtype to iterate over its range in a for loop. That raises the question of how to write an iteration when the array has an anonymous range for its bounds, since there is no name to refer to the range. Ada solves that via several attributes of array objects:

    
    
    
        
with Ada.Text_IO; use Ada.Text_IO; procedure Range_Example is type My_Int is range 0 .. 1000; type My_Int_Array is array (1 .. 5) of My_Int; Tab : constant My_Int_Array := (2, 3, 5, 7, 11); begin for I in Tab'Range loop -- ^ Gets the range of Tab Put (My_Int'Image (Tab (I))); end loop; New_Line; end Range_Example;

If you want more fine grained control, you can use the separate attributes 'First and 'Last.

    
    
    
        
with Ada.Text_IO; use Ada.Text_IO; procedure Array_Attributes_Example is type My_Int is range 0 .. 1000; type My_Int_Array is array (1 .. 5) of My_Int; Tab : My_Int_Array := (2, 3, 5, 7, 11); begin for I in Tab'First .. Tab'Last - 1 loop -- ^ Iterate on every index -- except the last Put (My_Int'Image (Tab (I))); end loop; New_Line; end Array_Attributes_Example;

The 'Range, 'First and 'Last attributes in these examples could also have been applied to the array type name, and not just the array instances.

Although not illustrated in the above examples, another useful attribute for an array instance A is A'Length, which is the number of elements that A contains.

It is legal and sometimes useful to have a "null array", which contains no elements. To get this effect, define an index range whose upper bound is less than the lower bound.

Unconstrained arrays

Let's now consider one of the most powerful aspects of Ada's array facility.

Every array type we have defined so far has a fixed size: every instance of this type will have the same bounds and therefore the same number of elements and the same size.

However, Ada also allows you to declare array types whose bounds are not fixed: in that case, the bounds will need to be provided when creating instances of the type.

    
    
    
        
with Ada.Text_IO; use Ada.Text_IO; procedure Unconstrained_Array_Example is type Days is (Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday); type Workload_Type is array (Days range <>) of Natural; -- Indefinite array type -- ^ Bounds are of type Days, -- but not known Workload : constant Workload_Type (Monday .. Friday) := -- ^ Specify the bounds -- when declaring (Friday => 7, others => 8); -- ^ Default value -- ^ Specify element by name of index begin for I in Workload'Range loop Put_Line (Integer'Image (Workload (I))); end loop; end Unconstrained_Array_Example;

The fact that the bounds of the array are not known is indicated by the Days range <> syntax. Given a discrete type Discrete_Type, if we use Discrete_Type for the index in an array type then Discrete_Type serves as the type of the index and comprises the range of index values for each array instance.

If we define the index as Discrete_Type range <> then Discrete_Type serves as the type of the index, but different array instances may have different bounds from this type.

An array type that is defined with the Discrete_Type range <> syntax for its index is referred to as an unconstrained array type, and, as illustrated above, the bounds need to be provided when an instance is created.

The above example also shows other forms of the aggregate syntax. You can specify associations by name, by giving the value of the index on the left side of an arrow association. 1 => 2 thus means "assign value 2 to the element at index 1 in my array". others => 8 means "assign value 8 to every element that wasn't previously assigned in this aggregate".

Attention

The so-called "box" notation (<>) is commonly used as a wildcard or placeholder in Ada. You will often see it when the meaning is "what is expected here can be anything".

In other languages

While unconstrained arrays in Ada might seem similar to variable length arrays in C, they are in reality much more powerful, because they're truly first-class values in the language. You can pass them as parameters to subprograms or return them from functions, and they implicitly contain their bounds as part of their value. This means that it is useless to pass the bounds or length of an array explicitly along with the array, because they are accessible via the 'First, 'Last, 'Range and 'Length attributes explained earlier.

Although different instances of the same unconstrained array type can have different bounds, a specific instance has the same bounds throughout its lifetime. This allows Ada to implement unconstrained arrays efficiently; instances can be stored on the stack and do not require heap allocation as in languages like Java.

Predefined array type: String

A recurring theme in our introduction to Ada types has been the way important built-in types like Boolean or Integer are defined through the same facilities that are available to the user. This is also true for strings: The String type in Ada is a simple array.

Here is how the string type is defined in Ada:

type String is
  array (Positive range <>) of Character;

The only built-in feature Ada adds to make strings more ergonomic is custom literals, as we can see in the example below.

Hint

String literals are a syntactic sugar for aggregates, so that in the following example, A and B have the same value.

    
    
    
        
package String_Literals is -- Those two declarations are equivalent A : String (1 .. 11) := "Hello World"; B : String (1 .. 11) := ('H', 'e', 'l', 'l', 'o', ' ', 'W', 'o', 'r', 'l', 'd'); end String_Literals;
    
    
    
        
with Ada.Text_IO; use Ada.Text_IO; procedure Greet is Message : String (1 .. 11) := "dlroW olleH"; -- ^ Pre-defined array type. -- Component type is Character begin for I in reverse Message'Range loop -- ^ Iterate in reverse order Put (Message (I)); end loop; New_Line; end Greet;

However, specifying the bounds of the object explicitly is a bit of a hassle; you have to manually count the number of characters in the literal. Fortunately, Ada gives you an easier way.

You can omit the bounds when creating an instance of an unconstrained array type if you supply an initialization, since the bounds can be deduced from the initialization expression.

    
    
    
        
with Ada.Text_IO; use Ada.Text_IO; procedure Greet is Message : constant String := "dlroW olleH"; -- ^ Bounds are automatically -- computed from -- initialization value begin for I in reverse Message'Range loop Put (Message (I)); end loop; New_Line; end Greet;
    
    
    
        
with Ada.Text_IO; use Ada.Text_IO; procedure Main is type Integer_Array is array (Natural range <>) of Integer; My_Array : constant Integer_Array := (1, 2, 3, 4); -- ^^^^^^^^^^^^^^^^^^^^^ -- Bounds are automatically -- computed from -- initialization value begin null; end Main;

Attention

As you can see above, the standard String type in Ada is an array. As such, it shares the advantages and drawbacks of arrays: a String value is stack allocated, it is accessed efficiently, and its bounds are immutable.

If you want something akin to C++'s std::string, you can use Unbounded Strings from Ada's standard library. This type is more like a mutable, automatically managed string buffer to which you can add content.

Restrictions

A very important point about arrays: bounds have to be known when instances are created. It is for example illegal to do the following.

declare
   A : String;
begin
   A := "World";
end;

Also, while you of course can change the values of elements in an array, you cannot change the array's bounds (and therefore its size) after it has been initialized. So this is also illegal:

declare
   A : String := "Hello";
begin
   A := "World";       --  OK: Same size
   A := "Hello World"; --  Not OK: Different size
end;

Also, while you can expect a warning for this kind of error in very simple cases like this one, it is impossible for a compiler to know in the general case if you are assigning a value of the correct length, so this violation will generally result in a run-time error.

Attention

While we will learn more about this later, it is important to know that arrays are not the only types whose instances might be of unknown size at compile-time.

Such objects are said to be of an indefinite subtype, which means that the subtype size is not known at compile time, but is dynamically computed (at run time).

    
    
    
        
with Ada.Text_IO; use Ada.Text_IO; procedure Indefinite_Subtypes is function Get_Number return Integer is begin return Integer'Value (Get_Line); end Get_Number; A : String := "Hello"; -- Indefinite subtype B : String (1 .. 5) := "Hello"; -- Definite subtype C : String (1 .. Get_Number); -- Indefinite subtype -- (Get_Number's value is computed at -- run-time) begin null; end Indefinite_Subtypes;

Here, the 'Value attribute converts the string to an integer.

Returning unconstrained arrays

The return type of a function can be any type; a function can return a value whose size is unknown at compile time. Likewise, the parameters can be of any type.

For example, this is a function that returns an unconstrained String:

    
    
    
        
with Ada.Text_IO; use Ada.Text_IO; procedure Main is type Days is (Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday); function Get_Day_Name (Day : Days := Monday) return String is begin return (case Day is when Monday => "Monday", when Tuesday => "Tuesday", when Wednesday => "Wednesday", when Thursday => "Thursday", when Friday => "Friday", when Saturday => "Saturday", when Sunday => "Sunday"); end Get_Day_Name; begin Put_Line ("First day is " & Get_Day_Name (Days'First)); end Main;

(This example is for illustrative purposes only. There is a built-in mechanism, the 'Image attribute for scalar types, that returns the name (as a String) of any element of an enumeration type. For example Days'Image(Monday) is "MONDAY".)

In other languages

Returning variable size objects in languages lacking a garbage collector is a bit complicated implementation-wise, which is why C and C++ don't allow it, preferring to depend on explicit dynamic allocation / free from the user.

The problem is that explicit storage management is unsafe as soon as you want to collect unused memory. Ada's ability to return variable size objects will remove one use case for dynamic allocation, and hence, remove one potential source of bugs from your programs.

Rust follows the C/C++ model, but with safe pointer semantics. However, dynamic allocation is still used. Ada can benefit from a possible performance edge because it can use any model.

Declaring arrays (2)

While we can have array types whose size and bounds are determined at run time, the array's component type needs to be of a definite and constrained type.

Thus, if you need to declare, for example, an array of strings, the String subtype used as component will need to have a fixed size.

    
    
    
        
with Ada.Text_IO; use Ada.Text_IO; procedure Show_Days is type Days is (Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday); subtype Day_Name is String (1 .. 2); -- Subtype of string with known size type Days_Name_Type is array (Days) of Day_Name; -- ^ Type of the index -- ^ Type of the element. -- Must be definite Names : constant Days_Name_Type := ("Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"); -- Initial value given by aggregate begin for I in Names'Range loop Put_Line (Names (I)); end loop; end Show_Days;

Array slices

One last feature of Ada arrays that we're going to cover is array slices. It is possible to take and use a slice of an array (a contiguous sequence of elements) as a name or a value.

    
    
    
        
with Ada.Text_IO; use Ada.Text_IO; procedure Main is Buf : String := "Hello ..."; Full_Name : String := "John Smith"; begin Buf (7 .. 9) := "Bob"; -- Careful! This works because the string -- on the right side is the same length as -- the replaced slice! -- Prints "Hello Bob" Put_Line (Buf); -- Prints "Hi John" Put_Line ("Hi " & Full_Name (1 .. 4)); end Main;

As we can see above, you can use a slice on the left side of an assignment, to replace only part of an array.

A slice of an array is of the same type as the array, but has a different subtype, constrained by the bounds of the slice.

Attention

Ada has multidimensional arrays, which are not covered in this course. Slices will only work on one dimensional arrays.

Renaming

So far, we've seen that the following elements can be renamed: subprograms, packages, and record components. We can also rename objects by using the renames keyword. This allows for creating alternative names for these objects. Let's look at an example:

    
    
    
        
package Measurements is subtype Degree_Celsius is Float; Current_Temperature : Degree_Celsius; end Measurements;
with Ada.Text_IO; use Ada.Text_IO; with Measurements; procedure Main is subtype Degrees is Measurements.Degree_Celsius; T : Degrees renames Measurements.Current_Temperature; begin T := 5.0; Put_Line (Degrees'Image (T)); Put_Line (Degrees'Image (Measurements.Current_Temperature)); T := T + 2.5; Put_Line (Degrees'Image (T)); Put_Line (Degrees'Image (Measurements.Current_Temperature)); end Main;

In the example above, we declare a variable T by renaming the Current_Temperature object from the Measurements package. As you can see by running this example, both Current_Temperature and its alternative name T have the same values:

  • first, they show the value 5.0

  • after the addition, they show the value 7.5.

This is because they are essentially referring to the same object, but with two different names.

Note that, in the example above, we're using Degrees as an alias of Degree_Celsius. We discussed this method earlier in the course.

Renaming can be useful for improving the readability of more complicated array indexing. Instead of explicitly using indices every time we're accessing certain positions of the array, we can create shorter names for these positions by renaming them. Let's look at the following example:

    
    
    
        
package Colors is type Color is (Black, Red, Green, Blue, White); type Color_Array is array (Positive range <>) of Color; procedure Reverse_It (X : in out Color_Array); end Colors;
package body Colors is procedure Reverse_It (X : in out Color_Array) is begin for I in X'First .. (X'Last + X'First) / 2 loop declare Tmp : Color; X_Left : Color renames X (I); X_Right : Color renames X (X'Last + X'First - I); begin Tmp := X_Left; X_Left := X_Right; X_Right := Tmp; end; end loop; end Reverse_It; end Colors;
with Ada.Text_IO; use Ada.Text_IO; with Colors; use Colors; procedure Test_Reverse_Colors is My_Colors : Color_Array (1 .. 5) := (Black, Red, Green, Blue, White); begin for C of My_Colors loop Put_Line ("My_Color: " & Color'Image (C)); end loop; New_Line; Put_Line ("Reversing My_Color..."); New_Line; Reverse_It (My_Colors); for C of My_Colors loop Put_Line ("My_Color: " & Color'Image (C)); end loop; end Test_Reverse_Colors;

In the example above, package Colors implements the procedure Reverse_It by declaring new names for two positions of the array. The actual implementation becomes easy to read:

begin
   Tmp     := X_Left;
   X_Left  := X_Right;
   X_Right := Tmp;
end;

Compare this to the alternative version without renaming:

begin
   Tmp                      := X (I);
   X (I)                    := X (X'Last +
                               X'First - I);
   X (X'Last + X'First - I) := Tmp;
end;