Containers

Aggregate aspect

Note

This feature was introduced in Ada 2022.

In a previous chapter, we discussed container aggregates, which are commonly used with standard containers. If you look at the type declarations of the standard containers (in the Ada.Containers packages), you'll notice that some of them make use of the Aggregate aspect. This aspect is used to specify which subprograms are called to process a container aggregate for a data type, let's say a type named T. Suppose we declare an object Obj like this: Obj : T := [1, 2, 3]. In this case, the Aggregate aspect specifies which subprograms are going to be called to process the [1, 2, 3] aggregate.

The Aggregate aspect is used in many declarations of the Ada.Containers packages. However, this aspect isn't restricted to the standard containers: we may indeed use the Aggregate aspect to specify a custom container aggregate for any type other than an array. In this section, we discuss the elements of the Aggregate aspect and how to use this aspect to create your own container aggregates.

In the Ada Reference Manual

Basic syntax

The Aggregate aspect has the following syntax:

type T is private
  with Aggregate =>
    (Empty          => Empty_Func,
     Add_Named      => Add_Named_Proc,
     Add_Unnamed    => Add_Unnamed_Proc,
     New_Indexed    => New_Indexed_Func,
     Assign_Indexed => Assign_Indexed_Proc);

Note that the order of the elements must be exactly as shown above.

Basically, there are three elements you can use in the Aggregate aspect to specify a procedure that is called when adding an element to the container: Add_Named, ada:Add_Unnamed, and Assign_Indexed.

Attention

Remember that an indexed aggregate has an index associated with each component. As discussed in the section on container aggregates,

  • for indexed positional container aggregates, the index of each component is implied by its position;

  • for indexed named container aggregates, the index of each component is explicitly indicated.

We discuss this topic later in more details.

Some restrictions apply to the Aggregate aspect. For example:

  • we have to specify at least one of those elements (Add_Named, Add_Unnamed, or Assign_Indexed), and

  • we cannot specify both Add_Named and Add_Unnamed elements at the same time.

We can, however, combine Add_Unnamed and Assign_Indexed in the same aspect declaration.

Classification

We can classify container aggregates in two categories:

  • whether they are indexed or not; and

  • whether they are positional or named.

This classification depends on the elements that were used in the declaration of the Aggregate aspect and whether a key is used in the aggregate. The following table presents the classification:

Indexed

Elements in Aggregate

Positional / named

Uses key

Container aggregate: example

No

Add_Named

Named

Yes

["Key_1" => "Hello", "Key_2" => "World"]

Add_Unnamed

Positional

No

["Hello", "World"]

Assign_Indexed Add_Unnamed

Yes

Assign_Indexed Add_Unnamed

Named

Yes

[1 => "Hello", 2 => "World"]

Assign_Indexed

Named

Yes

[1 => "Hello", 2 => "World"]

Positional

No

["Hello", "World"]

The next table presents the typical use-cases:

Category

Typical use

Add_Named

Maps

Add_Unnamed

Lists, sets

Add_Unnamed Assign_Indexed

Vectors

Assign_Indexed

(none)

Before we discuss these approaches, let's first look at the Empty element.

Empty

The Empty element allows us to specify the behavior for an empty container, i.e. the simplest version of a container without any components.

Let's assume we a container type T for which we specify an Empty function in the Aggregate aspect, and we declare an object Obj : T. In this case, the Empty function is called in one of two scenarios:

  • when we assign a null container to Obj — by writing Obj := []; — or

  • when we assign a container with at least one component to Obj — for example: Obj := [1, 2];.

Let's see a complete code example:

    
    
    
        
pragma Ada_2022; package Custom_Container_Aggregates is type T is private with Aggregate => (Empty => Empty_Func, Add_Named => Add_Named_Proc); function Empty_Func return T; procedure Add_Named_Proc (Cont : in out T; Key : String; Value : String) is null; private type T is record Cnt : Natural; end record; end Custom_Container_Aggregates;
with Ada.Text_IO; use Ada.Text_IO; package body Custom_Container_Aggregates is function Empty_Func return T is begin Put_Line ("Calling Empty_Func"); return (Cnt => 0); end Empty_Func; end Custom_Container_Aggregates;
pragma Ada_2022; with Ada.Text_IO; use Ada.Text_IO; with Custom_Container_Aggregates; use Custom_Container_Aggregates; procedure Show_Container_Aggregate_Empty is A : T; begin Put_Line ("A := []"); A := []; end Show_Container_Aggregate_Empty;

In this example, we specify the Empty function for the Aggregate aspect of the container type T. (We also use the Add_Unnamed element. You can ignore it for the moment: we'll discuss it later on.)

The A := [] statement in the Show_Container_Aggregate_Empty procedure calls Empty_Func — the function specified in the Empty element of the Aggregate aspect —, which returns an object of the container type T, which is then assigned to A. (You can confirm this by running this example and seeing the Calling Empty_Func message, which we included in the body of the Empty_Func function.)

We can also use a constant for the Empty element instead of a function:

    
    
    
        
pragma Ada_2022; package Custom_Container_Aggregates is type T is private with Aggregate => (Empty => Empty_Const, Add_Named => Add_Named_Proc); Empty_Const : constant T; procedure Add_Named_Proc (Cont : in out T; Key : String; Value : String) is null; private type T is record Cnt : Natural; end record; Empty_Const : constant T := (Cnt => 0); end Custom_Container_Aggregates;

Here, we simply assign Empty_Const when an actual Empty is needed.

In addition to this, we can specify a signed integer parameter — which indicates the number of components — for the Empty function:

    
    
    
        
pragma Ada_2022; package Custom_Container_Aggregates is type T is private with Aggregate => (Empty => Empty_Func, Add_Unnamed => Add_Unnamed_Proc); T_Len_Typical : constant := 10; function Empty_Func (Total : Integer := T_Len_Typical) return T; procedure Add_Unnamed_Proc (Cont : in out T; Item : String) is null; private type T is record Cnt : Natural; Total : Integer; end record; end Custom_Container_Aggregates;
with Ada.Text_IO; use Ada.Text_IO; package body Custom_Container_Aggregates is function Empty_Func (Total : Integer := T_Len_Typical) return T is begin Put_Line ("Calling Empty_Func (" & "Total => " & Total'Image & ")"); return (Total => Total, Cnt => 0); end Empty_Func; end Custom_Container_Aggregates;
pragma Ada_2022; with Ada.Text_IO; use Ada.Text_IO; with Custom_Container_Aggregates; use Custom_Container_Aggregates; procedure Show_Container_Aggregate_Empty is A : T; begin Put_Line ("A := []"); A := []; Put_Line ("A := [""Hello"", ""World""]"); A := ["Hello", "World"]; end Show_Container_Aggregate_Empty;

In this example, we specify an Empty_Func function with an Integer parameter (for the Empty element of the Aggregate aspect).

The actual argument for the integer parameter of the Empty_Func function depends on the number of elements we use in the container aggregate. In this specific example, when we write A := [], then Empty_Func (0) is called, whereas when we write A := ["Hello", "World"], this results in a call to Empty_Func (2).

Add_Named

The Add_Named element of the Aggregate aspect refers to a procedure that is called when we have a named container aggregate — i.e. a container aggregate with components in the Key => Value form — that doesn't use indexing.

Note that, when we specify the Add_Named element, we cannot specify any of these elements: Add_Unnamed, New_Indexed or Assign_Indexed. In other words, when we specify the Add_Named element, we can only use the Empty element in the same declaration.

    
    
    
        
pragma Ada_2022; package Custom_Container_Aggregates is type T is private with Aggregate => (Empty => Empty_Func, Add_Named => Add_Named_Proc); T_Len_Typical : constant := 10; function Empty_Func (Total : Integer := T_Len_Typical) return T; procedure Add_Named_Proc (Cont : in out T; Key : String; Value : String); private type T is record Total : Integer; Cnt : Natural; end record; end Custom_Container_Aggregates;
with Ada.Text_IO; use Ada.Text_IO; package body Custom_Container_Aggregates is function Empty_Func (Total : Integer := T_Len_Typical) return T is begin Put_Line ("Calling Empty_Func (" & "Total => " & Total'Image & ")"); return (Total => Total, Cnt => 0); end Empty_Func; procedure Add_Named_Proc (Cont : in out T; Key : String; Value : String) is begin Put_Line ("Calling Add_Named_Proc (Anon, " & "Key => """ & Key & """, " & "Value => """ & Value & """)"); end Add_Named_Proc; end Custom_Container_Aggregates;
pragma Ada_2022; with Ada.Text_IO; use Ada.Text_IO; with Custom_Container_Aggregates; use Custom_Container_Aggregates; procedure Show_Named_Container_Aggregate is A : T; begin Put_Line ("A := []"); A := []; Put_Line ("A := [""Key_1"" => ""Hello"", " & """Key_2"" => ""World""]"); A := ["Key_1" => "Hello", "Key_2" => "World"]; end Show_Named_Container_Aggregate;

When we write A := [], we're just calling Empty_Func (0) — as we're using a null container aggregate, there are no components to be added to the container. However, when we write A := ["Key_1" => "Hello", "Key_2" => "World"], we see the following calls:

  • a call to Empty_Func (2) that creates an empty container with two components;

  • a call to Add_Named_Proc (Anon, "Key_1", "Hello") for the first component, and

  • a call to Add_Named_Proc (Anon, "Key_2", "World") for the second component.

The Anon argument in the calls above indicates that an anonymous object is first created and then assigned to A.

Add_Unnamed

The Add_Unnamed element of the Aggregate aspect refers to a procedure that is called when we have a positional container aggregate.

Let's look at an example:

    
    
    
        
pragma Ada_2022; package Custom_Container_Aggregates is type T is private with Aggregate => (Empty => Empty_Func, Add_Unnamed => Add_Unnamed_Proc); T_Len_Typical : constant := 10; function Empty_Func (Total : Integer := T_Len_Typical) return T; procedure Add_Unnamed_Proc (Cont : in out T; Item : String); private type T is record Total : Integer; Cnt : Natural; end record; end Custom_Container_Aggregates;
with Ada.Text_IO; use Ada.Text_IO; package body Custom_Container_Aggregates is function Empty_Func (Total : Integer := T_Len_Typical) return T is begin Put_Line ("Calling Empty_Func (" & "Total => " & Total'Image & ")"); return (Total => Total, Cnt => 0); end Empty_Func; procedure Add_Unnamed_Proc (Cont : in out T; Item : String) is begin Put_Line ("Calling Add_Unnamed_Proc (Anon, " & "Item => """ & Item & """)"); end Add_Unnamed_Proc; end Custom_Container_Aggregates;
pragma Ada_2022; with Ada.Text_IO; use Ada.Text_IO; with Custom_Container_Aggregates; use Custom_Container_Aggregates; procedure Show_Unnamed_Container_Aggregate is A : T; begin Put_Line ("A := []"); A := []; Put_Line ("A := [""Hello"", ""World""]"); A := ["Hello", "World"]; end Show_Unnamed_Container_Aggregate;

The A := ["Hello", "World"] statement from the code above generates the following calls:

  • a call to Empty_Func (2) that creates an empty container with two components;

  • a call to Add_Unnamed_Proc (Anon, "Hello") for the first component, and

  • a call to Add_Unnamed_Proc (Anon, "World") for the second component.

Assign_Indexed

The Assign_Indexed element of the Aggregate aspect refers to a procedure that is called when we have an indexed container aggregate. Note that, when we specify the Assign_Indexed element, we must also use the New_Indexed element in the same aspect declaration.

Let's look at an example:

    
    
    
        
pragma Ada_2022; package Custom_Container_Aggregates is type T is private with Aggregate => (Empty => Empty_Func, New_Indexed => New_Indexed_Func, Assign_Indexed => Assign_Indexed_Proc); T_Len_Typical : constant := 10; function Empty_Func (Total : Integer := T_Len_Typical) return T; function New_Indexed_Func (First, Last : Positive) return T with Pre => First = Positive'First; procedure Assign_Indexed_Proc (Cont : in out T; Index : Positive; Item : String); private type T is record Total : Integer; Cnt : Natural; end record; end Custom_Container_Aggregates;
with Ada.Text_IO; use Ada.Text_IO; package body Custom_Container_Aggregates is function Empty_Func (Total : Integer := T_Len_Typical) return T is begin Put_Line ("Calling Empty_Func (" & "Total => " & Total'Image & ")"); return (Total => Total, Cnt => 0); end Empty_Func; function New_Indexed_Func (First, Last : Positive) return T is begin Put_Line ("Calling New_Indexed_Func (" & "First => " & First'Image & ", " & "Last => " & Last'Image & ")"); return (Total => Last - First + 1, Cnt => 0); end New_Indexed_Func; procedure Assign_Indexed_Proc (Cont : in out T; Index : Positive; Item : String) is pragma Unreferenced (Cont); begin Put_Line ("Calling Assign_Indexed_Proc (Anon, " & "Index => " & Index'Image & ", " & "Item => """ & Item & """)"); end Assign_Indexed_Proc; end Custom_Container_Aggregates;
pragma Ada_2022; with Ada.Text_IO; use Ada.Text_IO; with Custom_Container_Aggregates; use Custom_Container_Aggregates; procedure Show_Indexed_Container_Aggregate is A : T; begin Put_Line ("A := []"); A := []; Put_Line ("A := [""Hello"", ""World""]"); A := ["Hello", "World"]; Put_Line ("A := [1 => ""Hello"", " & "2 => ""World""]"); A := [1 => "Hello", 2 => "World"]; Put_Line ("A := [1 => ""Hello"", " & "2 => <>, " & "3 => ""World""]"); A := [1 => "Hello", 2 => <>, 3 => "World"]; end Show_Indexed_Container_Aggregate;

The A := [1 => "Hello", 2 => "World"] statement from the code above generates the following calls:

  • a call to New_Indexed_Func (First => 1, Last => 2) that creates an empty container with two components (where the first index of the container is 1 and the last index is 2);

  • a call to Assign_Indexed_Proc (Anon, 1, "Hello") for the first component (which is stored at the position with index 1), and

  • a call to Assign_Indexed_Proc (Anon, 2, "World") for the second component (which is stored at the position with index 2).

Note that, in the case of indexed aggregates, the New_Indexed_Func function is called instead of the Empty function.

For indexed aggregates, we can use the <> syntax for individual components. In the code above, we use it in the A := [1 => "Hello", 2 => <>, 3 => "World"] statement, which generates the following calls:

  • a call to New_Indexed_Func (First => 1, Last => 3) that creates an empty container with three components (where the first index of the container is 1 and the last index is 3);

  • a call to Assign_Indexed_Proc (Anon, 1, "Hello") for the first component, and

  • a call to Assign_Indexed_Proc (Anon, 3, "World") for the third component.

In other words, the 2 => <> element from the statement allows us to allocate a container with more components than we assign to. (There's no assignment happening at index 2 in the aggregate above: it'll have the default value or remain uninitialized.)

Combining Add_Named and Assign_Indexed

As mentioned previously, we may specify both Add_Named and Assign_Indexed elements together in the same aspect declaration. For example:

    
    
    
        
pragma Ada_2022; package Custom_Container_Aggregates is type T is private with Aggregate => (Empty => Empty_Func, Add_Unnamed => Add_Unnamed_Proc, New_Indexed => New_Indexed_Func, Assign_Indexed => Assign_Indexed_Proc); T_Len_Typical : constant := 10; function Empty_Func (Total : Integer := T_Len_Typical) return T; procedure Add_Unnamed_Proc (Cont : in out T; Item : String); function New_Indexed_Func (First, Last : Positive) return T with Pre => First = Positive'First; procedure Assign_Indexed_Proc (Cont : in out T; Index : Positive; Item : String); private type T is record Total : Integer; Cnt : Natural; end record; end Custom_Container_Aggregates;
with Ada.Text_IO; use Ada.Text_IO; package body Custom_Container_Aggregates is function Empty_Func (Total : Integer := T_Len_Typical) return T is begin Put_Line ("Calling Empty_Func (" & "Total => " & Total'Image & ")"); return (Total => Total, Cnt => 0); end Empty_Func; procedure Add_Unnamed_Proc (Cont : in out T; Item : String) is begin Put_Line ("Calling Add_Unnamed_Proc (Anon, " & "Item => """ & Item & """)"); end Add_Unnamed_Proc; function New_Indexed_Func (First, Last : Positive) return T is begin Put_Line ("Calling New_Indexed_Func (" & "First => " & First'Image & ", " & "Last => " & Last'Image & ")"); return (Total => Last - First + 1, Cnt => 0); end New_Indexed_Func; procedure Assign_Indexed_Proc (Cont : in out T; Index : Positive; Item : String) is pragma Unreferenced (Cont); begin Put_Line ("Calling Assign_Indexed_Proc (Anon, " & "Index => " & Index'Image & ", " & "Item => """ & Item & """)"); end Assign_Indexed_Proc; end Custom_Container_Aggregates;
pragma Ada_2022; with Ada.Text_IO; use Ada.Text_IO; with Custom_Container_Aggregates; use Custom_Container_Aggregates; procedure Show_Unnamed_Indexed_Container_Aggregate is A : T; begin Put_Line ("A := []"); A := []; Put_Line ("A := [""Hello"", ""World""]"); A := ["Hello", "World"]; Put_Line ("A := [1 => ""Hello"", 2 => ""World""]"); A := [1 => "Hello", 2 => "World"]; end Show_Unnamed_Indexed_Container_Aggregate;

Now, the subprogram calls depend on whether the container aggregate is positional or not:

  • for positional aggregates (e.g.: ["Hello", "World"]), the Add_Unnamed element is used; while

  • for named aggregates ([1 => "Hello", 2 => "World"]), the New_Indexed / Assign_Indexed elements are used.

User-Defined Iterator Types

Todo

Complete section!