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:
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 lineNew_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.
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.
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.
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.
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.
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:
If you want more fine grained control, you can use the separate attributes
'First
and 'Last
.
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.
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.
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.
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).
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
:
(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 an eventual 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.
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.
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:
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:
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;