Records

Default Initialization

As mentioned in the Introduction to Ada course, record components can have default initial values. Also, we've seen that other kinds of types can have default values.

In the Ada Reference Manual, we refer to these default initial values as "default expressions of record components." The term default expression indicates that we can use any kind of expression for the default initialization of record components — which includes subprogram calls for example:

    
    
    
        
package Show_Default_Initialization is function Init return Integer is (42); type Rec is record A : Integer := Init; end record; end Show_Default_Initialization;

In this example, the A component is initialized by default by a call to the Init procedure.

In the Ada Reference Manual

Dependencies

Default expressions cannot depend on other components. For example, if we have two components A and B, we cannot initialize B based on the value that A has:

    
    
    
        
package Show_Default_Initialization_Dependency is function Init return Integer is (42); type Rec is record A : Integer := Init; B : Integer := Rec.A; -- Illegal! end record; end Show_Default_Initialization_Dependency;

In this example, we cannot initialize the B component based on the value of the A component. (In fact, the syntax Rec.A as a way to refer to the A component is only allowed in predicates, not in the record component declaration.)

Todo

Add link to section on predicates once it's available.

Initialization Order

The default initialization of record components is performed in arbitrary order. In fact, the order is decided by the compiler, so we don't have control over it.

Let's see an example:

    
    
    
        
package Simple_Recs is function Init (S : String; I : Integer) return Integer; type Rec is record A : Integer := Init ("A", 1); B : Integer := Init ("B", 2); end record; end Simple_Recs;
with Ada.Text_IO; use Ada.Text_IO; package body Simple_Recs is function Init (S : String; I : Integer) return Integer is begin Put_Line (S & ": " & I'Image); return I; end Init; end Simple_Recs;
with Simple_Recs; use Simple_Recs; procedure Show_Initialization_Order is R : Rec; begin null; end Show_Initialization_Order;

When running this code example, you might see this:

A: 1
B: 2

However, the compiler is allowed to rearrange the operations, so this output is possible as well:

B: 2
A: 1

Therefore, we must write the default expression of each individual record components in such a way that the resulting initialization value is always correct, independently of the order that those expressions are evaluated.

Evaluation

According to the Annotated Ada Reference Manual, the "default expression of a record component is only evaluated upon the creation of a default-initialized object of the record type." This means that the default expression is by itself not evaluated when we declare the record type, but when we create an object of this type. It follows from this rule that the default is only evaluated when necessary, i.e,, when an explicit initial value is not specified in the object declaration.

Let's see an example:

    
    
    
        
with Ada.Text_IO; use Ada.Text_IO; with Simple_Recs; use Simple_Recs; procedure Show_Initialization_Order is begin Put_Line ("Some processing first..."); Put_Line ("Now, let's declare an object " & "of the record type Rec..."); declare R : Rec; begin Put_Line ("An object of Rec type has " & "just been created."); end; end Show_Initialization_Order;

Here, we only see the information displayed by the Init function — which is called to initialize the A and B components of the R record — during the object creation. In other words, the default expressions Init ("A", 1) and Init ("B", 2) are not evaluated when we declare the R type, but when we create an object of this type.

In the Ada Reference Manual

Defaults and object declaration

Note

This subsection was originally written by Robert A. Duff and published as Gem #12: Limited Types in Ada 2005.

Consider the following type declaration:

    
    
    
        
package Type_Defaults is type Color_Enum is (Red, Blue, Green); type T is private; private type T is record Color : Color_Enum := Red; Is_Gnarly : Boolean := False; Count : Natural; end record; procedure Do_Something; end Type_Defaults;

If we want to say, "make Count equal 100, but initialize Color and Is_Gnarly to their defaults", we can do this:

    
    
    
        
package body Type_Defaults is Object_100 : constant T := (Color => <>, Is_Gnarly => <>, Count => 100); procedure Do_Something is null; end Type_Defaults;

Historically

Prior to Ada 2005, the following style was common:

    
    
    
        
package body Type_Defaults is Object_100 : constant T := (Color => Red, Is_Gnarly => False, Count => 100); procedure Do_Something is null; end Type_Defaults;

Here, we only wanted Object_100 to be a default-initialized T, with Count equal to 100. It's a little bit annoying that we had to write the default values Red and False twice. What if we change our mind about Red, and forget to change it in all the relevant places? Since Ada 2005, the <> notation comes to the rescue, as we've just seen.

On the other hand, if we want to say, "make Count equal 100, but initialize all other components, including the ones we might add next week, to their defaults", we can do this:

    
    
    
        
package body Type_Defaults is Object_100 : constant T := (Count => 100, others => <>); procedure Do_Something is null; end Type_Defaults;

Note that if we add a component Glorp : Integer; to type T, then the others case leaves Glorp undefined just as this code would do:

    
    
    
        
package body Type_Defaults is procedure Do_Something is Object_100 : T; begin Object_100.Count := 100; end Do_Something; end Type_Defaults;

Therefore, you should be careful and think twice before using others.

Advanced Usages

In addition to expressions such as subprogram calls, we can use per-object expressions for the default value of a record component. (We discuss this topic later on in more details.)

For example:

    
    
    
        
package Rec_Per_Object_Expressions is type T (D : Positive) is private; private type T (D : Positive) is record V : Natural := D - 1; -- ^^^^^ -- Per-object expression end record; end Rec_Per_Object_Expressions;

In this example, component V is initialized by default with the per-object expression D - 1, where D refers to the discriminant D.

Mutually dependent types

In this section, we discuss how to use incomplete types to declare mutually dependent types. Let's start with this example:

    
    
    
        
package Mutually_Dependent is type T1 is record B : T2; end record; type T2 is record A : T1; end record; end Mutually_Dependent;

When you try to compile this example, you get a compilation error. The first problem with this code is that, in the declaration of the T1 record, the compiler doesn't know anything about T2. We could solve this by declaring an incomplete type (type T2;) before the declaration of T1. This, however, doesn't solve all the problems in the code: the compiler still doesn't know the size of T2, so we cannot create a component of this type. We could, instead, declare an access type and use it here. By doing this, even though the compiler doesn't know the size of T2, it knows the size of an access type designating T2, so the record component can be of such an access type.

To summarize, in order to solve the compilation error above, we need to:

  • use at least one incomplete type;

  • declare at least one component as an access to an object.

For example, we could declare an incomplete type T2 and then declare the component B of the T1 record as an access to T2. This is the corrected version:

    
    
    
        
package Mutually_Dependent is type T2; type T2_Access is access T2; type T1 is record B : T2_Access; end record; type T2 is record A : T1; end record; end Mutually_Dependent;

We could strive for consistency and declare two incomplete types and two accesses, but this isn't strictly necessary in this case. Here's the adapted code:

    
    
    
        
package Mutually_Dependent is type T1; type T1_Access is access T1; type T2; type T2_Access is access T2; type T1 is record B : T2_Access; end record; type T2 is record A : T1_Access; end record; end Mutually_Dependent;

Later on, we'll see that these code examples can be written using anonymous access types.

In the Ada Reference Manual

Null records

A null record is a record that doesn't have any components. Consequently, it cannot store any information. When declaring a null record, we simply write null instead of declaring actual components, as we usually do for records. For example:

    
    
    
        
package Null_Recs is type Null_Record is record null; end record; end Null_Recs;

Note that the syntax can be simplified to is null record, which is much more common than the previous form:

    
    
    
        
package Null_Recs is type Null_Record is null record; end Null_Recs;

Although a null record doesn't have components, we can still specify subprograms for it. For example, we could specify an addition operation for it:

    
    
    
        
package Null_Recs is type Null_Record is null record; function "+" (A, B : Null_Record) return Null_Record; end Null_Recs;
package body Null_Recs is function "+" (A, B : Null_Record) return Null_Record is pragma Unreferenced (A, B); begin return (null record); end "+"; end Null_Recs;
with Null_Recs; use Null_Recs; procedure Show_Null_Rec is A, B : Null_Record; begin B := A + A; A := A + B; end Show_Null_Rec;

In the Ada Reference Manual

Simple Prototyping

A null record doesn't provide much functionality on itself, as we're not storing any information in it. However, it's far from being useless. For example, we can make use of null records to design an API, which we can then use in an application without having to implement the actual functionality of the API. This allows us to design a prototype without having to think about all the implementation details of the API in the first stage.

Consider this example:

    
    
    
        
package Devices is type Device is private; function Create (Active : Boolean) return Device; procedure Reset (D : out Device) is null; procedure Process (D : in out Device) is null; procedure Activate (D : in out Device) is null; procedure Deactivate (D : in out Device) is null; private type Device is null record; function Create (Active : Boolean) return Device is (null record); end Devices;
with Ada.Text_IO; use Ada.Text_IO; with Devices; use Devices; procedure Show_Device is A : Device; begin Put_Line ("Creating device..."); A := Create (Active => True); Put_Line ("Processing on device..."); Process (A); Put_Line ("Deactivating device..."); Deactivate (A); Put_Line ("Activating device..."); Activate (A); Put_Line ("Resetting device..."); Reset (A); end Show_Device;

In the Devices package, we're declaring the Device type and its primitive subprograms: Create, Reset, Process, Activate and Deactivate. This is the API that we use in our prototype. Note that, although the Device type is declared as a private type, it's still defined as a null record in the full view.

In this example, the Create function, implemented as an expression function in the private part, simply returns a null record. As expected, this null record returned by Create matches the definition of the Device type.

All procedures associated with the Device type are implemented as null procedures, which means they don't actually have an implementation nor have any effect. We'll discuss this topic later on in the course.

In the Show_Device procedure — which is an application that implements our prototype —, we declare an object of Device type and call all subprograms associated with that type.

Extending the prototype

Because we're either using expression functions or null procedures in the specification of the Devices package, we don't have a package body for it (as there's nothing to be implemented). We could, however, move those user messages from the Show_Devices procedure to a dummy implementation of the Devices package. This is the adapted code:

    
    
    
        
package Devices is type Device is null record; function Create (Active : Boolean) return Device; procedure Reset (D : out Device); procedure Process (D : in out Device); procedure Activate (D : in out Device); procedure Deactivate (D : in out Device); end Devices;
with Ada.Text_IO; use Ada.Text_IO; package body Devices is function Create (Active : Boolean) return Device is pragma Unreferenced (Active); begin Put_Line ("Creating device..."); return (null record); end Create; procedure Reset (D : out Device) is pragma Unreferenced (D); begin Put_Line ("Processing on device..."); end Reset; procedure Process (D : in out Device) is pragma Unreferenced (D); begin Put_Line ("Deactivating device..."); end Process; procedure Activate (D : in out Device) is pragma Unreferenced (D); begin Put_Line ("Activating device..."); end Activate; procedure Deactivate (D : in out Device) is pragma Unreferenced (D); begin Put_Line ("Resetting device..."); end Deactivate; end Devices;
with Devices; use Devices; procedure Show_Device is A : Device; begin A := Create (Active => True); Process (A); Deactivate (A); Activate (A); Reset (A); end Show_Device;

As we changed the specification of the Devices package to not use null procedures, we now need a corresponding package body for it. In this package body, we implement the operations on the Device type, which actually just display a user message indicating which operation is being called.

Let's focus on this updated version of the Show_Device procedure. Now that we've removed all those calls to Put_Line from this procedure and just have the calls to operations associated with the Device type, it becomes more apparent that, even though Device is just a null record, we can design an application with a sequence of various commands operating on it. Also, when we just read the source-code of the Show_Device procedure, there's no clear indication that the Device type doesn't actually hold any information.

More complex applications

As we've just seen, we can use null records like any other type and create complex prototypes with them. We could, for instance, design an application that makes use of many null records, or even have types that depend on or derive from null records. Let's see a simple example:

    
    
    
        
package Many_Devices is type Device is null record; type Device_Config is null record; function Create (Config : Device_Config) return Device is (null record); type Derived_Device is new Device; procedure Process (D : Derived_Device) is null; end Many_Devices;
with Many_Devices; use Many_Devices; procedure Show_Derived_Device is A : Device; B : Derived_Device; C : Device_Config; begin A := Create (Config => C); B := Create (Config => C); Process (B); end Show_Derived_Device;

In this example, the Create function has a null record parameter (of Device_Config type) and returns a null record (of Device type). Also, we derive the Derived_Device type from the Device type. Consequently, Derived_Device is also a null record (since it's derived from a null record). In the Show_Derived_Device procedure, we declare objects of those types (A, B and C) and call primitive subprograms to operate on them.

This example shows that, even though the types we've declared are just null records, they can still be used to represent dependencies in our application.

Implementing the API

Let's focus again on the previous example. After we have an initial prototype, we can start implementing some of the functionality needed for the Device type. For example, we can store information about the current activation state in the record:

    
    
    
        
package Devices is type Device is private; function Create (Active : Boolean) return Device; procedure Reset (D : out Device); procedure Process (D : in out Device); procedure Activate (D : in out Device); procedure Deactivate (D : in out Device); private type Device is record Active : Boolean; end record; end Devices;
with Ada.Text_IO; use Ada.Text_IO; package body Devices is function Create (Active : Boolean) return Device is pragma Unreferenced (Active); begin Put_Line ("Creating device..."); return (Active => Active); end Create; procedure Reset (D : out Device) is pragma Unreferenced (D); begin Put_Line ("Processing on device..."); end Reset; procedure Process (D : in out Device) is pragma Unreferenced (D); begin Put_Line ("Deactivating device..."); end Process; procedure Activate (D : in out Device) is begin Put_Line ("Activating device..."); D.Active := True; end Activate; procedure Deactivate (D : in out Device) is begin Put_Line ("Resetting device..."); D.Active := False; end Deactivate; end Devices;
with Ada.Text_IO; use Ada.Text_IO; with Devices; use Devices; procedure Show_Device is A : Device; begin A := Create (Active => True); Process (A); Deactivate (A); Activate (A); Reset (A); end Show_Device;

Now, the Device record contains an Active component, which is used in the updated versions of Create, Activate and Deactivate.

Note that we haven't done any change to the implementation of the Show_Device procedure: it's still the same application as before. As we've been hinting in the beginning, using null records makes it easy for us to first create a prototype — as we did in the Show_Device procedure — and postpone the API implementation to a later phase of the project.

Tagged null records

A null record may be tagged, as we can see in this example:

    
    
    
        
package Null_Recs is type Tagged_Null_Record is tagged null record; type Abstract_Tagged_Null_Record is abstract tagged null record; end Null_Recs;

As we see in this example, a type can be tagged, or even abstract tagged. We discuss abstract types later on in the course.

As expected, in addition to deriving from tagged types, we can also extend them. For example:

    
    
    
        
package Devices is type Device is private; function Create (Active : Boolean) return Device; type Derived_Device is private; private type Device is tagged null record; function Create (Active : Boolean) return Device is (null record); type Derived_Device is new Device with record Active : Boolean; end record; function Create (Active : Boolean) return Derived_Device is (Active => Active); end Devices;

In this example, we derive Derived_Device from the Device type and extend it with the Active component. (Because we have a type extension, we also need to override the Create function.)

Since we're now introducing elements from object-oriented programming, we could consider using interfaces instead of null records. We'll discuss this topic later on in the course.

Record discriminants

We introduced the topic of record discriminants in the Introduction to Ada course. Also, in a previous chapter, we mentioned that record types with unconstrained discriminants without defaults are indefinite types.

In this section, we discuss a couple of details about record discriminants that we haven't covered yet. Although the discussion will be restricted to record discriminants, keep in mind that tasks and protected types can also have discriminants. We'll focus on discriminants for tasks and protected types in separate chapters.

Todo

Add link to section on task discriminants once it's available.

In addition, discriminants can be used to write per-object expressions. We discuss this topic later in this chapter.

In the Ada Reference Manual

Known and unknown discriminant parts

When it comes to discriminants, a type declaration falls into one of the following three categories: it has either no discriminants at all, known discriminants or unknown discriminants.

In order to have no discriminants, a type simply doesn't have a discriminant part in its declaration. For example:

    
    
    
        
package Show_Discriminants is type T_No_Discr is private; -- ^^^ -- no discriminant part private type T_No_Discr is null record; end Show_Discriminants;

By using parentheses after the type name, we're defining a discriminant part. In this case, the type can either have unknown or known discriminants. For example:

    
    
    
        
package Show_Discriminants is type T_Unknown_Discr (<>) is -- ^^ -- Unknown discriminant private; type T_Known_Discr (D : Integer) is -- ^^^^^^^^^^^ -- Known discriminant private; private type T_Unknown_Discr is null record; type T_Known_Discr (D : Integer) is null record; end Show_Discriminants;

An unknown discriminant part is represented by (<>) in the partial view — this is basically the so-called box notation <> (also known as box compound delimiter) in parentheses. We discuss unknown discriminant parts and their peculiarities later on in this chapter. In this section, we mainly focus on known discriminants.

Todo

Add link to section on unknown discriminant parts once it's available.

We've already seen examples of known discriminants in previous chapters. In simple terms, known discriminants are composed by one or more discriminant specifications, which are similar to subprogram parameters, but without parameter modes. In fact, we can think of discriminants as parameters for a type T, but with the goal of defining specific characteristics or constraints when declaring objects of type T.

Discriminant as constant property

We can think of discriminants as constant properties of a type. In fact, if you want to specify a record component C that shouldn't change, declaring it constant isn't allowed in Ada:

    
    
    
        
package Constant_Properties is type Rec is record C : constant Integer; -- ^^^^^^^^ -- ERROR: record components -- cannot be constant. V : Integer; end record; end Constant_Properties;

A simple solution is to use a record discriminant:

    
    
    
        
package Constant_Properties is type Rec (C : Integer) is record V : Integer; end record; end Constant_Properties;

A record discriminant can be accessed as a normal component, but it is read-only, so we cannot change it:

    
    
    
        
with Ada.Text_IO; use Ada.Text_IO; with Constant_Properties; use Constant_Properties; procedure Show_Constant_Property is R : Rec (10); begin Put_Line ("R.C = " & R.C'Image); R.C := R.C + 1; -- ERROR: cannot change -- record discriminant end Show_Constant_Property;

In this code example, the compilation fails because we cannot change the C discriminant. In this sense, C is a basically a constant component of the R object.

Private types

As we've seen in previous chapters, private types can have discriminants. For example:

    
    
    
        
package Private_With_Discriminants is type T (L : Positive) is private; private type Integer_Array is array (Positive range <>) of Integer; type T (L : Positive) is record Arr : Integer_Array (1 .. L); end record; end Private_With_Discriminants;

Here, discriminant L is used to specify the constraints of the array component Arr. Note that the same discriminant part must appear in both the partial and the full view of type T.

Object declaration

As we've already seen, we declare objects of a type T with a discriminant D by specifying the actual value of discriminant D. For example:

    
    
    
        
package Recs is type T (L : Positive; M : Positive) is null record; end Recs;
with Ada.Text_IO; use Ada.Text_IO; with Recs; use Recs; procedure Show_Object_Declaration is A : T (L => 5, M => 6); B : T (7, 8); begin Put_Line ("A.L = " & A.L'Image); Put_Line ("A.M = " & A.M'Image); Put_Line ("B.L = " & B.L'Image); Put_Line ("B.M = " & B.M'Image); end Show_Object_Declaration;

As we can see in the declaration of objects A and B, for the discriminant values, we can use a positional ((7, 8)) or named association ((L => 5, M => 6)).

Object size

Discriminants can have an impact on the object size because we can set the discriminant to constraint a component of an indefinite subtype. For example:

    
    
    
        
package Recs is type Null_Rec (L : Positive; M : Positive) is private; type Rec_Array (L : Positive) is private; private type Null_Rec (L : Positive; M : Positive) is null record; type Integer_Array is array (Positive range <>) of Integer; type Rec_Array (L : Positive) is record Arr : Integer_Array (1 .. L); end record; end Recs;
with Ada.Text_IO; use Ada.Text_IO; with Recs; use Recs; procedure Show_Object_Sizes is Null_Rec_A : Null_Rec (1, 2); Null_Rec_B : Null_Rec (5, 6); Rec_Array_A : Rec_Array (10); Rec_Array_B : Rec_Array (20); begin Put_Line ("Null_Rec_A'Size = " & Null_Rec_A'Size'Image); Put_Line ("Null_Rec_B'Size = " & Null_Rec_B'Size'Image); Put_Line ("Rec_Array_A'Size = " & Rec_Array_A'Size'Image); Put_Line ("Rec_Array_B'Size = " & Rec_Array_B'Size'Image); end Show_Object_Sizes;

In this example, Null_Rec_A and Null_Rec_B have the same size because the type is a null record. However, Rec_Array_A and Rec_Array_B have different sizes because we're setting the L discriminant — which we use to constraint the Arr array component of the Rec_Array type — to 10 and 20, respectively.

Object assignments

As we've just seen, when we set the values for the discriminants of a type in the object declaration, we're constraining the objects. Those constraints are checked at runtime by the discriminant check. If the discriminants don't match, the Constraint_Error exception is raised.

Let's see an example:

    
    
    
        
package Recs is type T (L : Positive; M : Positive) is null record; end Recs;
with Recs; use Recs; procedure Show_Object_Assignments is A1, A2 : T (5, 6); B : T (7, 8); begin A1 := A2; -- OK B := A1; -- ERROR! end Show_Object_Assignments;

In this example, the A1 := A2 assignment is accepted because both A1 and A2 have the same constraints ((5, 6)). However, the B := A1 assignment is not accepted because the discriminant check fails at runtime.

Note that the discriminant check is not performed when we use mutable subtypes — we discuss this specific kind of subtypes later on.

Discriminant type

In a discriminant specification, the type of the discriminant can only be a discrete subtype or an access type. Other kinds of types — e.g. composite types such as record types — are illegal for discriminants. However, we can always use them indirectly by using access types. (We'll see an example later.)

In addition to that, we can also use a different kind of access types, namely anonymous access-to-object subtypes. This specific kind of discriminant is called access discriminant. We discuss this topic in more details in another chapter.

Let's see a code example:

    
    
    
        
package Recs is type Usage_Mode is (Off, Simple_Usage, Advanced_Usage); type Priv_Info is private; type Priv_Info_Access is access Priv_Info; type Proc_Access is access procedure (P : in out Priv_Info); type Priv_Rec (Last : Positive; Usage : Usage_Mode; Info : Priv_Info_Access; Proc : Proc_Access) is private; private type Priv_Info is record A : Positive; B : Positive; end record; type Priv_Rec (Last : Positive; Usage : Usage_Mode; Info : Priv_Info_Access; Proc : Proc_Access) is null record; end Recs;

In this example, we're declaring the Priv_Rec type with the following discriminants:

As indicated previously, it's illegal to use a private type or a record type as the type of a discriminant. For example:

    
    
    
        
package Recs is type Priv_Info is private; type Priv_Rec (Info : Priv_Info) is private; -- ^^^^^^^^^^^^^^^^ -- ERROR: cannot use private type -- in discriminant. private type Priv_Info is record A : Positive; B : Positive; end record; type Priv_Rec (Info : Priv_Info) is null record; end Recs;

We cannot use the Priv_Info directly as a discriminant type because it's a private type. However, as we've just seen in the previous code example, we use it indirectly by using an access type to this private type (see Priv_Info_Access in the code example).

Indefinite subtypes as discriminants

As we already implied, we cannot use indefinite subtypes as discriminants. For example, the following code won't compile:

    
    
    
        
package Unconstrained_Types is type Integer_Array is array (Positive range <>) of Integer; type Simple_Record (Arr : Integer_Array) is -- ^^^^^^^^^^^^^^^^^^^ -- ERROR: cannot use indefinite type -- in discriminant. record L : Natural := Arr'Length; end record; end Unconstrained_Types;

Integer_Array is a correct type declaration — although the type itself is indefinite after the declaration. However, we cannot use it as the discriminant in the declaration of Simple_Record. We could, however, have a correct declaration by using discriminants as access values:

    
    
    
        
package Unconstrained_Types is type Integer_Array is array (Positive range <>) of Integer; type Integer_Array_Access is access Integer_Array; type Simple_Record (Arr : Integer_Array_Access) is record L : Natural := Arr'Length; end record; end Unconstrained_Types;

By adding the Integer_Array_Access type and using it in Simple_Record's type declaration, we can indirectly use an indefinite type in the declaration of another indefinite type. We discuss this topic later in another chapter.

Default values

We can specify default values for discriminants. Note, however, that we must either specify default values for all discriminants of the discriminant part or for none of them. This contrasts with default values for subprogram parameters, where we can specify default values for just a subset of all parameters of a specific subprogram.

As expected, we can override the default values by specifying the values of each discriminant when declaring an object. Let's see a simple example:

    
    
    
        
package Recs is type T (L : Positive := 1; M : Positive := 2) is private; private type T (L : Positive := 1; M : Positive := 2) is null record; end Recs;
with Ada.Text_IO; use Ada.Text_IO; with Recs; use Recs; procedure Show_Object_Declaration is A : T; B : T (7, 8); begin Put_Line ("A.L = " & A.L'Image); Put_Line ("A.M = " & A.M'Image); Put_Line ("B.L = " & B.L'Image); Put_Line ("B.M = " & B.M'Image); end Show_Object_Declaration;

In this example, object A makes use of the default values for the discriminants of type T, so it has the discriminants (L => 1, M => 2). In the case of object B, we're specifying the values (L => 7, M => 8), which are used instead of the default values.

Note that we cannot set default values for nonlimited tagged types. The same applies to generic formal types. For example:

Todo

Add link to section on generic formal types once it's available.

    
    
    
        
package Recs is type TT (L : Positive := 1; M : Positive := 2) is -- ^^^^^^^^^^^^^^^^^ -- ERROR: cannot assign default -- in discriminant of -- nonlimited tagged type. tagged private; type LTT (L : Positive := 1; M : Positive := 2) is tagged limited private; private type TT (L : Positive := 1; M : Positive := 2) is tagged null record; type LTT (L : Positive := 1; M : Positive := 2) is tagged limited null record; end Recs;

As we can see, compilation fails because of the default values for the discriminants of the nonlimited tagged type TT. In the case of the limited tagged type LTT, the default values for the discriminants are legal.

Mutable subtypes

An unconstrained discriminated subtype with defaults is called a mutable subtype, and a variable of such a subtype is called a mutable variable because the discriminants of such a variable can be changed. An important feature of mutable subtypes is that it allows for changing the discriminants of an object via assignments — in this case, no discriminant check is performed.

Let's see an example:

    
    
    
        
package Mutability is type T_Non_Mutable (L : Positive; M : Positive) is null record; type T_Mutable (L : Positive := 1; M : Positive := 2) is null record; end Mutability;
with Mutability; use Mutability; procedure Show_Mutable_Subtype_Assignment is NM_1 : T_Non_Mutable (5, 6); NM_2 : T_Non_Mutable (7, 8); M_1 : T_Mutable (7, 8); M_2 : T_Mutable; begin NM_2 := NM_1; -- ERROR! M_2 := M_1; -- OK end Show_Mutable_Subtype_Assignment;

In this example, the NM_2 := NM_1 assignment fails because both objects are of a non-mutable subtype with different discriminants, so that the discriminant check fails at runtime. However, the M_2 := M_1 assignment is OK because both objects are mutable variables. In this case, this assignment changes the discriminants of M_2 from (L => 1, M => 2) to (L => 7, M => 8).

Note that assignments of mutable variables may not always work at runtime. For example, if a discriminant of a mutable subtype is used to constraint a component of indefinite subtype, we might see the corresponding checks fail at runtime. For example:

    
    
    
        
package Mutability is type T_Mutable_Array (L : Positive := 10) is private; private type Integer_Array is array (Positive range <>) of Integer; type T_Mutable_Array (L : Positive := 10) is record Arr : Integer_Array (1 .. L); end record; end Mutability;
with Ada.Text_IO; use Ada.Text_IO; with Mutability; use Mutability; procedure Show_Mutable_Subtype_Error is A : T_Mutable_Array (10); B : T_Mutable_Array (20); begin Put_Line ("A'Size = " & A'Size'Image); Put_Line ("B'Size = " & B'Size'Image); A := B; -- ERROR! end Show_Mutable_Subtype_Error;

In this case, the assignment A := B raises the Constraint_Error exception at runtime. Here, the Arr component of each object has a different range: 1 .. 10 for object A and 1 .. 20 for object B. To prevent this situation, we should declare T_Mutable_Array as a limited type, so that assignments are not permitted.

Derived types and subtypes

As expected, we may derive types with discriminants or declare subtypes of it.

Subtypes

When declaring a subtype of a type with discriminants, we have the choice to specify the value of the discriminants for the parent type, or specify no discriminants at all:

    
    
    
        
package Subtypes_With_Discriminants is type T (L : Positive; M : Positive) is null record; subtype Sub_T is T; -- Discriminants are not specified: -- taking the ones from T. subtype Sub_T_2 is T (L => 3, M => 4); -- Discriminants are specified: -- taking the ones from Sub_T_2 end Subtypes_With_Discriminants;

For the Sub_T subtype declaration in this example, we don't specify values for the parent type's discriminants. For Sub_T_2, in contrast, we set the discriminants to (L => 3, M => 4).

When declaring objects of these subtypes, we need to take the constraints into account:

    
    
    
        
package Subtypes_With_Discriminants is type T (L : Positive; M : Positive) is null record; subtype Sub_T is T; -- Discriminants are not specified: -- taking the ones from T. subtype Sub_T_2 is T (L => 3, M => 4); -- Discriminants are specified: -- taking the ones from Sub_T_2 end Subtypes_With_Discriminants;
with Subtypes_With_Discriminants; use Subtypes_With_Discriminants; procedure Show_Subtypes_With_Discriminants is A1 : T (1, 2); A2 : T (3, 4); B1 : Sub_T (1, 2); B2 : Sub_T (3, 4); C2 : Sub_T_2; -- C1 : Sub_T_2 (1, 2); -- ^^^^ -- ERROR: discriminants already -- constrained begin B1 := A1; -- OK: discriminants match B2 := A1; -- CONSTRAINT_ERROR! B2 := A2; -- OK: discriminants match C2 := A1; -- CONSTRAINT_ERROR! C2 := A2; -- OK: discriminants match end Show_Subtypes_With_Discriminants;

For objects of Sub_T subtype, we have to specify the value of each discriminant. On the other hand, for objects of Sub_T_2 type, we cannot specify the constraints because they have already been defined in the subtype's declaration — in this case, they're always set to (3, 4).

When assigning objects of different subtypes, the discriminant check will be performed — as we mentioned before. In this example, the assignments B2 := A1 and C2 := A1 fail because the objects have different constraints.

Derived types

The behavior for derived types is very similar to the one we've just described for subtypes. For example:

    
    
    
        
package Derived_With_Discriminants is type T (L : Positive; M : Positive) is null record; type T_Derived is new T; -- Discriminants are not specified: -- taking the ones from T. type T_Derived_2 is new T (L => 3, M => 4); -- Discriminants are specified: -- taking the ones from T_Derived_2 end Derived_With_Discriminants;

For the T_Derived type, we reuse the discriminants of the parent type T. For the T_Derived_2 type, we specify a value for each discriminant of T.

As you probably notice, this code looks very similar to the code using subtypes. The main difference between using subtypes and derived types is that, as expected, we have to perform a type conversion in the assignments:

    
    
    
        
with Derived_With_Discriminants; use Derived_With_Discriminants; procedure Show_Derived_With_Discriminants is A1 : T (1, 2); A2 : T (3, 4); B1 : T_Derived (1, 2); B2 : T_Derived (3, 4); C2 : T_Derived_2; -- C1 : Sub_T_2 (1, 2); -- ^^^^ -- ERROR: discriminants already -- constrained begin B1 := T_Derived (A1); -- OK: discriminants match B2 := T_Derived (A1); -- ERROR! C2 := T_Derived_2 (A1); -- CONSTRAINT_ERROR! C2 := T_Derived_2 (A2); -- OK: discriminants match end Show_Derived_With_Discriminants;

Once again, a discriminant check is performed when assigning objects to ensure that the type discriminants match. In this code example, the assignments B2 := A1 and C2 := A1 fail because the objects have different constraints.

Derived types with renamed discriminants

We could rewrite a type declaration such as type T_Derived is new T by explicitly declaring the discriminants. We can do that for the previous code example:

    
    
    
        
package Derived_With_Discriminants is type T (L : Positive; M : Positive) is null record; -- The declaration: -- -- type T_Derived is new T; -- -- is the same as: -- type T_Derived (L : Positive; M : Positive) is new T (L => L, M => M); end Derived_With_Discriminants;

We may, however, rename the discriminants instead. For example, we could rename L and M to X and Y. For example:

    
    
    
        
package Derived_With_Discriminants is type T (L : Positive; M : Positive) is null record; type T_Derived (X : Positive; Y : Positive) is new T (L => X, M => Y); end Derived_With_Discriminants;

Of course, if we use named association when declaring objects, we have to use the correct discriminant names:

    
    
    
        
with Ada.Text_IO; use Ada.Text_IO; with Derived_With_Discriminants; use Derived_With_Discriminants; procedure Show_Derived_With_Discriminants is A : T (L => 1, M => 2); B : T_Derived (X => 3, Y => 4); -- ^^^^^^^^^^^^^^ -- Using correct discriminant names begin Put_Line ("A.L = " & A.L'Image); Put_Line ("A.M = " & A.M'Image); Put_Line ("B.X = " & B.X'Image); Put_Line ("B.Y = " & B.Y'Image); end Show_Derived_With_Discriminants;

In essence, the discriminants of both parent and derived types are the same: the only difference is that they are accessed by different names. This allows us to convert from a parent type to a derived type:

    
    
    
        
with Derived_With_Discriminants; use Derived_With_Discriminants; procedure Show_Derived_With_Discriminants is A : T (L => 1, M => 2); B : T_Derived (X => 1, Y => 2); begin B := T_Derived (A); -- OK end Show_Derived_With_Discriminants;

Here, even though objects A and B have discriminants with different names, the assignment B := T_Derived (A) is valid.

Derived types with more constrained discriminants

When deriving types with discriminants, we may use a more constrained type for the discriminants of derived type. For example, if the discriminant D of the parent type is of Integer type, the corresponding discriminant of the derived type may use a constrained subtype such as Natural or Positive — because both Natural and Positive are subtypes of type Integer. For example:

    
    
    
        
package Derived_With_Discriminants is type T (L : Integer; M : Integer) is null record; type T_Derived_2 (X : Natural; Y : Positive) is new T (L => X, M => Y); end Derived_With_Discriminants;

As expected, the constraints of each discriminant's type are taken into account when evaluating the value that is specified for each discriminant:

    
    
    
        
with Ada.Text_IO; use Ada.Text_IO; with Derived_With_Discriminants; use Derived_With_Discriminants; procedure Show_Derived_With_Discriminants is A : T (L => -1, M => -2); B : T_Derived_2 (X => 0, Y => 1); begin Put_Line ("A.L = " & A.L'Image); Put_Line ("A.M = " & A.M'Image); Put_Line ("B.X = " & B.X'Image); Put_Line ("B.Y = " & B.Y'Image); end Show_Derived_With_Discriminants;

Here, we can use (L => -1, M => -2) in the declaration of object A because both discriminants are of Integer type. However, in the declaration of object B, we can only use values for the discriminants that are in the range of the Natural and Positive subtypes, respectively. (If you change the code to use negative values instead, a Constraint_Error exception is raised at runtime.)

Extending the discriminant part

As we've seen, we can rename discriminants or use more constrained subtypes for discriminants in derived types. We might also want to add a new discriminant to the derived type — in addition to the discriminants of the parent's type. However, this is considered a type extension, as the new discriminant is part of the type definition.

As an example, we may want to add the A discriminant of Boolean type to a derived type. For non-tagged types, such a declaration will trigger a compilation error as expected:

    
    
    
        
package Derived_With_Discriminants is type T (L : Positive; M : Positive) is null record; type T_Derived (X : Positive; Y : Positive; A : Boolean) is -- ^^^^^^^^^^^ -- ERROR: cannot extend type with new -- Boolean discriminant A new T (L => X, M => Y); end Derived_With_Discriminants;

To circumvent this issue, we could, of course, declare a component of T type instead of deriving from it:

    
    
    
        
package Derived_With_Discriminants is type T (L : Positive; M : Positive) is null record; type T_2 (X : Positive; Y : Positive; A : Boolean) is record A_Comp : T (L => X, M => Y); end record; end Derived_With_Discriminants;

In this case, A_Comp is a component of type T, and we're using the discriminant X and Y as the constraints of this component.

Naturally, using tagged types is another alternative:

    
    
    
        
package Derived_With_Discriminants is type T (L : Positive; M : Positive) is tagged null record; type T_Derived_Extended (X : Positive; Y : Positive; A : Boolean) is -- New discriminant new T (L => X, M => Y) with null record; type T_Derived_Extended_2 (A : Boolean; -- New discriminant X : Positive; Y : Positive) is new T (L => X, M => Y) with null record; type T_Derived_Extended_3 (A : Boolean) is -- New discriminant new T (L => 1, M => 2) with null record; type T_Derived_Extended_4 (A : Boolean; -- New discriminant X : Positive) is new T (L => X, M => X) with null record; end Derived_With_Discriminants;

In this code example, we're adding the A discriminant when declaring T_Derived_Extended. Because T is a tagged type, such a new discriminant is fine.

Note that the order of the discriminants can be rearranged: when deriving a new type, we don't need to specify the discriminants of the parent type before any new discriminants. In fact, in the declaration of T_Derived_Extended_2, the additional discriminant A is declared before the discriminants that match the parent type's discriminants.

In addition, we may even use literals to specify the constraints for the parent type — as we're doing in the declaration of T_Derived_Extended_3. Also, we can use the same discriminant from the derived type for the constraints of the parent type — in the declaration of T_Derived_Extended_4, we use the X discriminant for both L and M discriminants of type T.

Deriving with defaults

If the discriminants of the parent type have default values, those default values are inherited by the derived type. Alternatively, we can set different default values.

Let's see a code example:

    
    
    
        
package Derived_With_Discriminants is type T (L : Positive := 1; M : Positive := 2) is null record; type T_Derived is new T; type T_Derived_2 (L : Positive := 1; M : Positive := 3) is new T (L => L, M => M); end Derived_With_Discriminants;

In this example, the derived type T_Derived has the same default values as the parent type T, namely (L => 1, M => 2). For the derived type T_Derived_2, we're changing the value of M to 3 and keeping the same value for L.

As we've seen before, instead of setting default values, we can set the constraints of the parent type in the declaration of the derived type:

    
    
    
        
package Derived_With_Discriminants is type T (L : Positive := 1; M : Positive := 2) is null record; type T_Derived_Constrainted is new T (L => 1, M => 3); end Derived_With_Discriminants;

In this case, we're constraining the discriminants of the parent type to (L => 1, M => 3). Note that L has the same value as the default value set for the parent type T.

For further reading...

In other contexts (such as record aggregates, which we discuss in another chapter), we could use the so-called box notation to specify that we want to use the default value. This, however, isn't possible with type discriminants:

    
    
    
        
package Derived_With_Discriminants is type T (L : Positive := 1; M : Positive := 2) is null record; type T_Derived_Constraint is new T (L => <>, M => 3); -- ^^^^^^^ -- ERROR: cannot use default values -- via box notation end Derived_With_Discriminants;

Instead of using <>, we have to repeat the value explicitly.

Per-Object Expressions

In record type declarations, we might want to define a component that makes use of a name that refers to a discriminant of the record type, or to the record type itself. An expression where we use such a name is called a per-object expression.

The term "per-object" comes from the fact that, in the component definition, we're referring to a piece of information that will be known just when creating an object of that type. For example, if the per-object expression refers to a discriminant of a type T, the actual value of that discriminant will only be specified when we declare an object of type T. Therefore, the component definition is specific for that individual object — but not necessarily for other objects of the same type, as we might use different values for the discriminant.

The constraint that contains a per-object expression is called a per-object constraint. The actual constraint of that component isn't completely known when we declare the record type, but only later on when an object of that type is created. (Note that the syntax of a constraint includes the parentheses or the keyword range.)

In addition to referring to discriminants, per-object expressions can also refer to the record type itself, as we'll see later.

Let's start with a simple record declaration:

    
    
    
        
package Rec_Per_Object_Expressions is type Stack (S : Positive) is private; private type Integer_Array is array (Positive range <>) of Integer; type Stack (S : Positive) is record Arr : Integer_Array (1 .. S); -- ^^^^^^ -- -- S -- ^ -- Per-object expression -- -- (1 .. S) -- ^^^^^^^^ -- Per-object constraint Top : Natural := 0; end record; end Rec_Per_Object_Expressions;

In this example, we see the Stack record type with a discriminant S. In the declaration of the Arr component of the that type, S is a per-object expression, as it refers to the S discriminant. Also, (1 .. S) is a per-object constraint.

Let's look at another example using anonymous access types:

    
    
    
        
package Rec_Per_Object_Expressions is type T is private; type T_Processor (Selected_T : access T) is private; private type T is null record; type T_Container (Selected_T : access T) is null record; type T_Processor (Selected_T : access T) is record E : T_Container (Selected_T); -- -- Selected_T -- ^^^^^^^^^^ -- Per-object expression -- -- (Selected_T) -- ^^^^^^^^^^^^ -- Per-object constraint end record; end Rec_Per_Object_Expressions;

Let's focus on the T_Processor type from this example. The Selected_T discriminant is being used in the definition of the E component. The per-object constraint is (Selected_T).

Finally, per-object expressions can also refer to the record type we're declaring. For example:

    
    
    
        
package Rec_Per_Object_Expressions is type T is limited private; private type T_Processor (Selected_T : access T) is null record; type T is limited record E : T_Processor (T'Access); -- -- T'Access -- ^^^^^^^^ -- Per-object expression -- -- (T'Access) -- ^^^^^^^^^^ -- Per-object constraint end record; end Rec_Per_Object_Expressions;

In this example, when we write T'Access within the declaration of the T record type, the actual value for the Access attribute will be known when an object of T type is created. In that sense, T'Access is a per-object expression — (T'Access) is the corresponding per-object constraint.

Note that T'Access is referring to the type within a type definition. This is generally treated as a reference to the object being created, the so-called current instance.

Todo

  • Add link to Adv_Ada_Reference_Current_Instance section ("Access Types: Reference to current instance") once it's available.

In the Ada Reference Manual

Default value

We can also use per-object expressions to calculate the default value of a record component:

    
    
    
        
package Rec_Per_Object_Expressions is type T (D : Positive) is private; private type T (D : Positive) is record V : Natural := D - 1; -- ^^^^^ -- Per-object expression S : Natural := D'Size; -- ^^^^^^ -- Per-object expression end record; end Rec_Per_Object_Expressions;

Here, we calculate the default value of V using the per-object expression D - 1, and the default of value of S using the per-object D'Size.

The default expression for a component of a discriminated record can be an arbitrary per-object expression. (This contrasts with important restrictions that exist for per-object constraints, as we discuss later on.) Such expressions might include function calls or uses of any defined operator. For this reason, the following code example is accepted by the compiler:

    
    
    
        
package Rec_Per_Object_Expressions is type Stack (S : Positive) is private; private type Integer_Array is array (Positive range <>) of Integer; type Stack (S : Positive) is record Arr : Integer_Array (1 .. S); Top : Natural := 0; Overflow_Warning : Positive := S * 9 / 10; -- ^^^^^^^^^^ -- Per-object expression -- using computation for -- the default expression. end record with Dynamic_Predicate => Overflow_Warning in (S + 1) / 2 .. S - 1; -- -- (S + 1) / 2 -- ^^^^^^^^^^^ -- Per-object expression -- using computation. -- -- S - 1 -- ^^^^^ -- Per-object expression -- using computation. end Rec_Per_Object_Expressions;

In this example, we can identify multiple per-object expressions that use a computation: S * 9 / 10, (S + 1) / 2, and S - 1.

Restrictions

There are some important restrictions on per-object constraints:

  1. Per-object range constraints such as 1 .. T'Size are not allowed.

    • For example, the following code example doesn't compile:

          
          
          
              
      package Rec_Per_Object_Expressions is type Bit_Field is array (Positive range <>) of Boolean with Pack; type T is record Arr : Bit_Field (1 .. T'Size); -- ^^^^^^ -- ERROR: per-object range constraint -- using the Size attribute -- is illegal. end record; end Rec_Per_Object_Expressions;
  2. Within a per-object index constraint or discriminant constraint, each per-object expression must be the name of a discriminant directly, without any further computation.

    • Therefore, we're allowed to write (1 .. S) — as we've seen in a previous example —. However, writing (1 .. S - 1) would be illegal.

    • For example, the following adaptation to the previous code example doesn't compile:

          
          
          
              
      package Rec_Per_Object_Expressions is type Stack (S : Positive) is private; private type Integer_Array is array (Natural range <>) of Integer; type Stack (S : Positive) is record Arr : Integer_Array (0 .. S - 1); -- ^^^^^ -- ERROR: computation in per-object -- expression is illegal. Top : Integer := -1; end record; end Rec_Per_Object_Expressions;

      In this example, using the computation S - 1 to specify the range of Arr isn't permitted. (Note that, as we've seen before, this restriction doesn't apply when the computation is used in a per-object expression that calculates the default value of a component.)

  3. We can only use access attributes (T'Access and T'Unchecked_Access) in per-object constraints.