Rationale for Ada 2005
8.2 Lists and vectors
We will first consider
the list container since in some ways it is the simplest.
Here is its specification interspersed with some explanation
generic
type Element_Type is private;
with function "=" (Left, Right: Element_Type) return Boolean is <>;
package Ada.Containers.Doubly_Linked_Lists is
pragma Preelaborate(Doubly_Linked_Lists);
type List is tagged private;
pragma Preelaborable_Initialization(List);
type Cursor is private;
pragma Preelaborable_Initialization(Cursor);
Empty_List: constant List;
No_Element: constant Cursor;
The two generic parameters are the type of the elements
in the list and the definition of equality for comparing elements. This
equality relation must be such that x = y
and y = x always have the same value.
A list container is
an object of the type List. It is tagged since
it will inevitably be implemented as a controlled type. The fact that
it is visibly tagged means that all the advantages of object oriented
programming are available. For one thing it enables the use of the prefixed
notation so that we can write operations such as
My_List.Append(Some_Value);
rather than
Append(My_List, Some_Value);
The type Cursor is an
important concept. It provides the means of access to individual elements
in the container. Not only does it contain a reference to an element
but it also identifies the container as well. This enables various checks
to be made to ensure that we don't accidentally meddle with an element
in the wrong container.
The constants Empty_List
and No_Element are as expected and also provide
default values for objects of types List and
Cursor respectively.
function "=" (Left, Right: List) return Boolean;
function Length(Container: List) return Count_Type;
function Is_Empty(Container: List) return Boolean;
procedure Clear(Container: in out List);
The function "=" compares two lists. It
only returns true if both lists have the same number of elements and
corresponding elements have the same value as determined by the generic
parameter "=" for comparing elements. The subprograms Length,
Is_Empty and Clear
are as expected.
Note that A_List = Empty_List,
Is_Empty(A_List) and Length(A_List)
= 0 all have the same value.
function Element(Position: Cursor) return Element_Type;
procedure Replace_Element(
Container: in out List; Position: in Cursor;
New_Item: in Element_Type);
These are the first operations we have met that use
a cursor. The function Element takes a cursor
and returns the value of the corresponding element (remember that a cursor
identifies the list as well as the element itself). The procedure Replace_Element
replaces the value of the element identified by the cursor by the value
given; it makes a copy of course.
Note carefully that Replace_Element
has both the list and cursor as parameters. There are two reasons for
this concerning correctness. One is to enable a check that the cursor
does indeed identify an element in the given list. The other is to ensure
that we do have write access to the container (the parameter has mode
in out). Otherwise it would be possible to modify a container
even though we only had a constant view of it. So as a general principle
any operation that modifies a container must have the container as a
parameter whereas an operation that only reads it such as the function
Element does not.
procedure Query_Element(
Position: in Cursor;
Process: not null access procedure (Element: in Element_Type));
procedure Update_Element(
Container: in out List; Position: in Cursor;
Process: not null access procedure (Element: in out Element_Type));
These procedures provide in situ access to
an element. One parameter is the cursor identifying the element and another
is an access to a procedure to be called with that element as parameter.
In the case of Query_Element, we can only
read the element whereas in the case of Update_Element
we can change it as well since the parameter mode of the access procedure
is in out. Note that Update_Element
also has the container as a parameter for reasons just mentioned when
discussing Replace_Element.
The reader might wonder whether there is any difference
between calling the function Element to obtain
the current value of an element and using the seemingly elaborate mechanism
of Query_Element. The answer is that the function
Element makes a copy of the value whereas
Query_Element gives access to the value without
making a copy. (And similarly for Replace_Element
and Update_Element.) This wouldn't matter
for a simple list of integers but it would matter if the elements were
large or of a controlled type (maybe even lists themselves).
procedure Move(Target, Source: in out List);
This moves the list from the source to the target
after first clearing the target. It does not make copies of the elements
so that after the operation the source is empty and Length(Source)
is zero.
procedure Insert(
Container: in out List;
Before: in Cursor;
New_Item: in Element_Type;
Count: in Count_Type := 1);
procedure Insert(
Container: in out List;
Before: in Cursor;
New_Item: in Element_Type;
Position: out Cursor;
Count: in Count_Type := 1);
procedure Insert(
Container: in out List;
Before: in Cursor;
Position: out Cursor;
Count: in Count_Type := 1);
These three procedures enable one or more identical
elements to be added anywhere in a list. The place is indicated by the
parameter Before – if this is No_Element,
then the new elements are added at the end. The second procedure is similar
to the first but also returns a cursor to the first of the added elements.
The third is like the second but the new elements take their default
values. Note the default value of one for the number of elements.
procedure Prepend(
Container: in out List;
New_Item: in Element_Type;
Count: in Count_Type := 1);
procedure Append(
Container: in out List;
New_Item: in Element_Type;
Count: in Count_Type := 1);
These add one or more new elements at the beginning
or end of a list respectively. Clearly these operations can be done using
Insert but they are sufficiently commonly
needed that it is convenient to provide them specially.
procedure Delete(
Container: in out List;
Position: in out Cursor;
Count: in Count_Type := 1);
procedure Delete_First(Container: in out List; Count: in Count_Type := 1);
procedure Delete_Last(Container: in out List; Count: in Count_Type := 1);
These delete one or more elements at the appropriate
position. In the case of Delete, the parameter
Position is set to No_Element
upon return. If there are not as many as Count
elements to be deleted at the appropriate place then it just deletes
as many as possible (this clearly results in the container becoming empty
in the case of Delete_First and Delete_Last).
procedure Reverse_Elements(Container: in out List);
This does the obvious thing. It would have been nice
to call this procedure Reverse but sadly that
is a reserved word.
procedure Swap(Container: in out List; I, J: in Cursor);
This handy procedure swaps the values in the two
elements denoted by the two cursors. The elements must be in the given
container otherwise Program_Error is raised.
Note that the cursors do not change.
procedure Swap_Links(Container: in out List; I, J: in Cursor);
This performs the low level operation of swapping
the links rather than the values which can be much faster if the elements
are large. There is no analogue in the vectors package.
procedure Splice(
Target: in out List;
Before: in Cursor;
Source in out List);
procedure Splice(
Target: in out List;
Before: in Cursor;
Source: in out List;
Position: in out Cursor);
procedure Splice(
Container: in out List;
Before: in Cursor;
Position: in Cursor);
These three procedures enable elements to be moved
(without copying). The place is indicated by the parameter Before
– if this is No_Element, then the elements
are added at the end. The first moves all the elements of Source
into Target at the position given by Before;
as a consequence, like the procedure Move,
after the operation the source is empty and Length(Source)
is zero. The second moves a single element at Position
from the list Source to Target
and so the length of target is incremented whereas that of source is
decremented; Position is updated to its new
location in Target. The third moves a single
element within a list and so the length remains the same (note the formal
parameter is Container rather than Target
in this case). There are no corresponding operations in the vectors package
because, like Swap_Links, we are just moving
the links and not copying the elements.
function First(Container: List) return Cursor;
function First_Element(Container: List) return Element_Type;
function Last(Container: List) return Cursor;
function Last_Element(Container: List) return Element_Type;
function Next(Position: Cursor) return Cursor;
function Previous(Position: Cursor) return Cursor;
procedure Next(Position: in out Cursor);
procedure Previous(Position: in out Cursor);
function Find(
Container: List;
Item: Element_Type;
Position: Cursor:= No_Element) return Cursor;
function Reverse_Find(
Container: List;
Item: Element_Type;
Position: Cursor:= No_Element) return Cursor;
function Contains(Container: List; Item: Element_Type) return Boolean;
Hopefully the purpose of these is almost self-evident.
The function Find searches for an element
with the given value starting at the given cursor position (or at the
beginning if the position is No_Element);
if no element is found then it returns No_Element.
Reverse_Find does the same but backwards.
Note that equality used for the comparison in Find
and Reverse_Find is that defined by the generic
parameter "=".
function Has_Element(Position: Cursor) return Boolean;
This returns False if
the cursor does not identify an element; for example if it is No_Element.
procedure Iterate(
Container: in List;
Process: not null access procedure (Position: in Cursor));
procedure Reverse_Iterate(
Container: in List;
Process: not null access procedure (Position: in Cursor));
These apply the procedure designated by the parameter
Process to each element of the container in
turn in the appropriate order.
generic
with function "<" (Left, Right: Element_Type) return Boolean is <>;
package Generic_Sorting is
function Is_Sorted(Container: List) return Boolean;
procedure Sort(Container: in out List);
procedure Merge(Target, Source: in out List);
end Generic_Sorting;
This generic package performs sort and merge operations
using the order specified by the generic formal parameter. Note that
we use generics rather than access to subprogram parameters when the
formal process is given by an operator. This is because the predefined
operations have convention Intrinsic and one
cannot pass an intrinsic operation as an access to subprogram parameter.
The function Is_Sorted returns True
if the container is already sorted. The procedure Sort
arranges the elements into order as necessary – note that no copying
is involved since it is only the links that are moved. The procedure
Merge takes the elements from Source
and adds them to Target. After the merge Length(Source)
is zero. If both lists were sorted before the merge then the result is
also sorted.
And finally we have
private
... -- not specified by the language
end Ada.Containers.Doubly_Linked_Lists;
If the reader has got
this far they have probably understood how to use this package so extensive
examples are unnecessary. However, as a taste, here is a simple stack
of floating point numbers
package Stack is
procedure Push(X: in Float);
function Pop return Float;
function Size return Integer;
Stack_Empty : exception;
end;
with Ada.Containers.Doubly_Linked_Lists;
use Ada.Containers;
package body Stack is
package Float_Container is new Doubly_Linked_Lists(Float);
use Float_Container;
The_Stack: List;
procedure Push(X: in Float) is
begin
Append(The_Stack, X); -- or The_Stack.Append(X);
end Push;
function Pop return Float is
Result: Float;
begin
if Is_Empty(The_Stack) then
raise Stack_Empty;
end if;
Result := Last_Element(The_Stack);
Delete_Last(The_Stack);
return Result;
end Pop;
function Size return Integer is
begin
return Integer(Length(The_Stack));
end Size;
end Stack;
This barely needs any explanation. The lists package
is instantiated in the package Stack and the
object The_Stack is of course the list container.
The rest is really straightforward. We could of course use the prefixed
notation throughout as indicated in Push.
An important point should be mentioned concerning
lists (and containers in general). This is that attempts to do foolish
things typically result in Constraint_Error
or Program_Error being raised. This especially
applies to the procedures Process in Query_Element,
Update_Element, Iterate
and Reverse_Iterate. The concepts of tampering
with cursors and elements are introduced in order to dignify a general
motto of "Thou shalt not violate thy container".
Tampering with cursors occurs when elements are added
to or deleted from a container (by calling Insert
and so on) whereas tampering with elements means replacing an element
(by calling Replace_Element for example).
Tampering with elements is a greater sin and includes tampering with
cursors. The procedure Process in Query_Element
and Update_Element must not tamper with elements
and the procedure Process in the other cases
must not tamper with cursors. The reader might think it rather odd that
Update_Element should not be allowed to tamper
with elements since the whole purpose is to update the element; this
comes back to the point mentioned earlier that update element gives access
to the existing element in situ via the parameter of Process
and that is allowed – calling Replace_Element
within Process would be tampering. Tampering
causes Program_Error to be raised.
We will now consider the vectors package.
Its specification starts
generic
type Index_Type is range <>;
type Element_Type is private;
with function "=" (Left, Right: Element_Type) return Boolean is <>;
package Ada.Containers.Vectors is
pragma Preelaborate(Vectors);
This is similar to the lists package except for the
additional generic parameter Index_Type (note
that this is an integer type and not a discrete type). This additional
parameter reflects the idea that a vector is essentially an array and
we can index directly into an array.
In fact the vectors
package enables us to access elements either by using an index or by
using a cursor. Thus many operations are duplicated such as
function Element(Container: Vector; Index: Index_Type) return Element_Type;
function Element(Position: Cursor) return Element_Type;
procedure Replace_Element(
Container: in out Vector;
Index: in Index_Type;
New_Item: in Element_Type);
procedure Replace_Element(
Container: in out Vector;
Position: in Cursor;
New_Item: in Element_Type);
If we use an index then there is always a distinct
parameter identifying the vector as well. If we use a cursor then the
vector parameter is omitted if the vector is unchanged as is the case
with the function Element. Remember that we
stated earlier that a cursor identifies both an element and the container
but if the container is being changed as in the case of Replace_Element
then the container has to be passed as well to ensure write access and
to enable a check that the cursor does identify an element in the correct
container.
There are also functions
First_Index and Last_Index
thus
function First_Index(Container: Vector) return Index_Type;
function Last_Index(Container: Vector) return Extended_Index;
These return the values of the index of the first
and last elements respectively. The function First_Index
always returns Index_Type'First whereas Last_Index
will return No_Index if the vector is empty.
The function Length returns Last_Index–First_Index+1
which is zero if the vector is empty. Note that the irritating subtype
Extended_Index has to be introduced in order
to cope with end values. The constant No_Index
has the value Extended_Index'First which is
equal to Index_Type'First–1.
There are operations
to convert between an index and a cursor thus
function To_Cursor(Container: Vector; Index: Extended_Index) return Cursor;
function To_Index(Position: Cursor) return Extended_Index;
It is perhaps slightly messier to use the index and
vector parameters because of questions concerning the range of values
of the index but probably slightly faster and maybe more familiar. And
sometimes of course using an index is the whole essence of the problem.
In the chapter on access types (see
3.4) we
showed a use of the procedure
Update_Element
to double the values of those elements of a vector whose index was in
the range 5 to 10. This would be tedious with cursors.
But an advantage of using cursors is that (provided
certain operations are avoided) it is easy to replace the use of vectors
by lists.
For example here is
the stack package rewritten to use vectors
with Ada.Containers.Vectors; -- changed
use Ada.Containers;
package body Stack is
package Float_Container is new Vectors(Natural,Float); -- changed
use Float_Container;
The_Stack: Vector; -- changed
procedure Push(X: in Float) is
begin
Append(The_Stack, X);
end Push;
-- etc exactly as before
end Stack;
So the changes are very few indeed and can be quickly
done with a simple edit.
Note that the index parameter has been given as Natural
rather than Integer. Using Integer
will not work since attempting to elaborate the subtype Extended_Index
would raise Constraint_Error when evaluating
Integer'First–1. But in any event it
is more natural for the index range of the container to start at 0
(or 1) rather than a large negative value
such as Integer'First.
There are other important
properties of vectors that should be mentioned. One is that there is
a concept of capacity. Vectors are adjustable and will extend if necessary
when new items are added. However, this might lead to lots of extensions
and copying and so we can set the capacity of a container by calling
procedure Reserve_Capacity(Container: in out Vector; Capacity: in Count_Type);
There is also
function Capacity(Container: Vector) return Count_Type;
which naturally returns the current capacity. Note
that Length(V) cannot exceed Capacity(V)
but might be much less.
If we add items to a vector whose length and capacity
are the same then no harm is done. The capacity will be expanded automatically
by effectively calling Reserve_Capacity internally.
So the user does not need to set the capacity although not doing so might
result in poorer performance.
There is also the concept
of "empty elements". These are elements whose values have not
been set. There is no corresponding concept with lists. It is a bounded
error to read an empty element. Empty elements arise if we declare a
vector by calling
function To_Vector(Length: Count_Type) return Vector;
as in
My_Vector: Vector := To_Vector(100);
There is also the much
safer
function To_Vector(New_Item: Element_Type; Length: Count_Type) return Vector;
which sets all the elements to the value New_Item.
There is also a procedure
procedure Set_Length(Container: in out Vector; Length: in Count_Type);
This changes the length of a vector. This may require
elements to be deleted (from the end) or to be added (in which case the
new ones are empty).
The final way to get
an empty element is by calling one of
procedure Insert_Space(
Container: in out Vector;
Before: in Extended_Index;
Count: in Count_Type := 1);
procedure Insert_Space(Container: in out Vector;
Before: in Cursor;
Position: out Cursor;
Count: in Count_Type := 1);
These insert the number of empty elements given by
Count at the place indicated. Existing elements
are slid along as necessary. These should not be confused with the versions
of Insert which do not provide an explicit
value for the elements – in those cases the new elements take their
default values.
Care needs to be taken if we use empty elements.
For example we should not compare two vectors using "="
if they have empty elements because this implies reading them. But the
big advantage of empty elements is that they provide a quick way to make
a large lump of space in a vector which can then be filled in with appropriate
values. One big slide is a lot faster than lots of little ones.
For completeness, we briefly mention the remaining
few subprograms that are unique to the vectors package.
There are further versions
of Insert thus
procedure Insert(
Container: in out Vector;
Before: in Extended_Index; New_Item: in Vector);
procedure Insert(
Container: in out Vector;
Before: in Cursor; New_Item: in Vector);
procedure Insert(
Container: in out Vector;
Before: in Cursor; New_Item: in Vector; Position: out Cursor);
These insert copies of a vector into another vector
(rather than just single elements).
There are also corresponding
versions of Prepend and Append
thus
procedure Prepend(Container: in out Vector; New_Item: in Vector);
procedure Append(Container: in out Vector; New_Item: in Vector);
Finally, there are
four functions "&" which concatenate
vectors and elements by analogy with those for the type String.
Their specifications are
function "&" (Left, Right: Vector) return Vector;
function "&" (Left: Vector; Right: Element_Type) return Vector;
function "&" (Left: Element_Type; Right: Vector) return Vector;
function "&" (Left, Right: Element_Type) return Vector;
Note the similarity
between
Append(V1, V2);
V1 := V1 & V2;
The result is the same but using "&"
is less efficient because of the extra copying involved. But "&"
is a familiar operation and so is provided for convenience.
© 2005, 2006 John Barnes Informatics.
Sponsored in part by: