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.)
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.
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.
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
.
This is called a
discriminant constraint.
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:
The
Last
discriminant of the scalar (i.e. discrete) typePositive
;The
Usage
discriminant of the enumeration (i.e. discrete) typeUsage_Mode
;The
Info
discriminant of the access-to-object typePriv_Info_Access
;We discuss access-to-object types as discriminant type in another chapter.
The
Proc
discriminant of the access-to-subprogram typeProc_Access
;We discuss access-to-subprogram types as discriminant type in another chapter.
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:
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. However, there are a couple of details associated with this, which we discuss now.
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;