Types and Representation
Enumeration Representation Clauses
We have talked about the internal code of an enumeration in another section. We may change this internal code by using a representation clause, which has the following format:
for Primary_Color is (Red => 1,
Green => 5,
Blue => 1000);
The value of each code in a representation clause must be distinct. However, as you can see above, we don't need to use sequential values — the values must, however, increase for each enumeration.
We can rewrite the previous example using a representation clause:
package Days is type Day is (Mon, Tue, Wed, Thu, Fri, Sat, Sun); for Day use (Mon => 2#00000001#, Tue => 2#00000010#, Wed => 2#00000100#, Thu => 2#00001000#, Fri => 2#00010000#, Sat => 2#00100000#, Sun => 2#01000000#); end Days;with Ada.Text_IO; use Ada.Text_IO; with Days; use Days; procedure Show_Days is begin for D in Day loop Put_Line (Day'Image (D) & " position = " & Integer'Image (Day'Pos (D))); Put_Line (Day'Image (D) & " internal code = " & Integer'Image (Day'Enum_Rep (D))); end loop; end Show_Days;
Now, the value of the internal code is the one that we've specified in the representation clause instead of being equivalent to the value of the enumeration position.
In the example above, we're using binary values for each enumeration — basically viewing the integer value as a bit-field and assigning one bit for each enumeration. As long as we maintain an increasing order, we can use totally arbitrary values as well. For example:
package Days is type Day is (Mon, Tue, Wed, Thu, Fri, Sat, Sun); for Day use (Mon => 5, Tue => 9, Wed => 42, Thu => 49, Fri => 50, Sat => 66, Sun => 99); end Days;
Data Representation
This section provides a glimpse on attributes and aspects used for data representation. They are usually used for embedded applications because of strict requirements that are often found there. Therefore, unless you have very specific requirements for your application, in most cases, you won't need them. However, you should at least have a rudimentary understanding of them. To read a thorough overview on this topic, please refer to the Introduction to Embedded Systems Programming course.
In the Ada Reference Manual
Sizes
Ada offers multiple attributes to retrieve the size of a type or an object:
Attribute |
Description |
---|---|
|
Size of the representation of a subtype or an object (in bits). |
|
Size of a component or an aliased object (in bits). |
|
Size of a component of an array (in bits). |
|
Number of storage elements reserved for an access type or a task object. |
For the first three attributes, the size is measured in bits. In the case of
Storage_Size
, the size is measured in storage elements. Note that the
size information depends your target architecture. We'll discuss some examples
to better understand the differences among those attributes.
Important
A storage element is the smallest element we can use to store data in memory. As we'll see soon, a storage element corresponds to a byte in many architectures.
The size of a storage element is represented by the
System.Storage_Unit
constant. In other words, the storage unit
corresponds to the number of bits used for a single storage element.
In typical architectures, System.Storage_Unit
is 8 bits. In this
specific case, a storage element is equal to a byte in memory. Note,
however, that System.Storage_Unit
might have a value different than
eight in certain architectures.
Size attribute and aspect
Let's start with a code example using the Size
attribute:
package Custom_Types is type UInt_7 is range 0 .. 127; type UInt_7_S32 is range 0 .. 127 with Size => 32; end Custom_Types;with Ada.Text_IO; use Ada.Text_IO; with Custom_Types; use Custom_Types; procedure Show_Sizes is V1 : UInt_7; V2 : UInt_7_S32; begin Put_Line ("UInt_7'Size: " & UInt_7'Size'Image); Put_Line ("UInt_7'Object_Size: " & UInt_7'Object_Size'Image); Put_Line ("V1'Size: " & V1'Size'Image); New_Line; Put_Line ("UInt_7_S32'Size: " & UInt_7_S32'Size'Image); Put_Line ("UInt_7_S32'Object_Size: " & UInt_7_S32'Object_Size'Image); Put_Line ("V2'Size: " & V2'Size'Image); end Show_Sizes;
Depending on your target architecture, you may see this output:
UInt_7'Size: 7
UInt_7'Object_Size: 8
V1'Size: 8
UInt_7_S32'Size: 32
UInt_7_S32'Object_Size: 32
V2'Size: 32
When we use the Size
attribute for a type T
, we're retrieving the
minimum number of bits necessary to represent objects of that type. Note that
this is not the same as the actual size of an object of type T
because
the compiler will select an object size that is appropriate for the target
architecture.
In the example above, the size of the UInt_7
is 7 bits, while the most
appropriate size to store objects of this type in the memory of our target
architecture is 8 bits. To be more specific, the range of UInt_7
(0 .. 127) can be perfectly represented in 7 bits. However, most target
architectures don't offer 7-bit registers or 7-bit memory storage, so 8 bits is
the most appropriate size in this case.
We can retrieve the size of an object of type T
by using the
Object_Size
. Alternatively, we can use the Size
attribute
directly on objects of type T
to retrieve their actual size — in
our example, we write V1'Size
to retrieve the size of V1
.
In the example above, we've used both the Size
attribute (for example,
UInt_7'Size
) and the Size
aspect (with Size => 32
).
While the size attribute is a function that returns the size, the size aspect
is a request to the compiler to verify that the expected size can be used on
the target platform. You can think of this attribute as a dialog between the
developer and the compiler:
(Developer) "I think that
UInt_7_S32
should be stored using at least 32 bits. Do you agree?"(Ada compiler) "For the target platform that you selected, I can confirm that this is indeed the case."
Depending on the target platform, however, the conversation might play out like this:
(Developer) "I think that
UInt_7_S32
should be stored using at least 32 bits. Do you agree?"(Ada compiler) "For the target platform that you selected, I cannot possibly do it! COMPILATION ERROR!"
Component size
Let's continue our discussion on sizes with an example that makes use of the
Component_Size
attribute:
package Custom_Types is type UInt_7 is range 0 .. 127; type UInt_7_Array is array (Positive range <>) of UInt_7; type UInt_7_Array_Comp_32 is array (Positive range <>) of UInt_7 with Component_Size => 32; end Custom_Types;with Ada.Text_IO; use Ada.Text_IO; with Custom_Types; use Custom_Types; procedure Show_Sizes is Arr_1 : UInt_7_Array (1 .. 20); Arr_2 : UInt_7_Array_Comp_32 (1 .. 20); begin Put_Line ("UInt_7_Array'Size: " & UInt_7_Array'Size'Image); Put_Line ("UInt_7_Array'Object_Size: " & UInt_7_Array'Object_Size'Image); Put_Line ("UInt_7_Array'Component_Size: " & UInt_7_Array'Component_Size'Image); Put_Line ("Arr_1'Component_Size: " & Arr_1'Component_Size'Image); Put_Line ("Arr_1'Size: " & Arr_1'Size'Image); New_Line; Put_Line ("UInt_7_Array_Comp_32'Object_Size: " & UInt_7_Array_Comp_32'Size'Image); Put_Line ("UInt_7_Array_Comp_32'Object_Size: " & UInt_7_Array_Comp_32'Object_Size'Image); Put_Line ("UInt_7_Array_Comp_32'Component_Size: " & UInt_7_Array_Comp_32'Component_Size'Image); Put_Line ("Arr_2'Component_Size: " & Arr_2'Component_Size'Image); Put_Line ("Arr_2'Size: " & Arr_2'Size'Image); New_Line; end Show_Sizes;
Depending on your target architecture, you may see this output:
UInt_7_Array'Size: 17179869176
UInt_7_Array'Object_Size: 17179869176
UInt_7_Array'Component_Size: 8
Arr_1'Component_Size: 8
Arr_1'Size: 160
UInt_7_Array_Comp_32'Size: 68719476704
UInt_7_Array_Comp_32'Object_Size: 68719476704
UInt_7_Array_Comp_32'Component_Size: 32
Arr_2'Component_Size: 32
Arr_2'Size: 640
Here, the value we get for Component_Size
of the UInt_7_Array
type is 8 bits, which matches the UInt_7'Object_Size
— as we've
seen in the previous subsection. In general, we expect the component size to
match the object size of the underlying type.
However, we might have component sizes that aren't equal to the object size of
the component's type. For example, in the declaration of the
UInt_7_Array_Comp_32
type, we're using the Component_Size
aspect
to query whether the size of each component can be 32 bits:
type UInt_7_Array_Comp_32 is
array (Positive range <>) of UInt_7
with Component_Size => 32;
If the code compiles, we see this value when we use the Component_Size
attribute. In this case, even though UInt_7'Object_Size
is 8 bits, the
component size of the array type (UInt_7_Array_Comp_32'Component_Size
)
is 32 bits.
Note that we can use the Component_Size
attribute with data types, as
well as with actual objects of that data type. Therefore, we can write
UInt_7_Array'Component_Size
and Arr_1'Component_Size
, for
example.
This big number (17179869176 bits) for UInt_7_Array'Size
and
UInt_7_Array'Object_Size
might be surprising for you. This is due to the
fact that Ada is reporting the size of the UInt_7_Array
type for the
case when the complete range is used. Considering that we specified a positive
range in the declaration of the UInt_7_Array
type, the maximum length
on this machine is 231 - 1. The object size of an array type is
calculated by multiplying the maximum length by the component size. Therefore,
the object size of the UInt_7_Array
type corresponds to the
multiplication of 231 - 1 components (maximum length) by 8 bits
(component size).
Storage size
To complete our discussion on sizes, let's look at this example of storage sizes:
package Custom_Types is type UInt_7 is range 0 .. 127; type UInt_7_Access is access UInt_7; end Custom_Types;with Ada.Text_IO; use Ada.Text_IO; with System; with Custom_Types; use Custom_Types; procedure Show_Sizes is AV1, AV2 : UInt_7_Access; begin Put_Line ("UInt_7_Access'Storage_Size: " & UInt_7_Access'Storage_Size'Image); Put_Line ("UInt_7_Access'Storage_Size (bits): " & Integer'Image (UInt_7_Access'Storage_Size * System.Storage_Unit)); Put_Line ("UInt_7'Size: " & UInt_7'Size'Image); Put_Line ("UInt_7_Access'Size: " & UInt_7_Access'Size'Image); Put_Line ("UInt_7_Access'Object_Size: " & UInt_7_Access'Object_Size'Image); Put_Line ("AV1'Size: " & AV1'Size'Image); New_Line; Put_Line ("Allocating AV1..."); AV1 := new UInt_7; Put_Line ("Allocating AV2..."); AV2 := new UInt_7; New_Line; Put_Line ("AV1.all'Size: " & AV1.all'Size'Image); New_Line; end Show_Sizes;
Depending on your target architecture, you may see this output:
UInt_7_Access'Storage_Size: 0
UInt_7_Access'Storage_Size (bits): 0
UInt_7'Size: 7
UInt_7_Access'Size: 64
UInt_7_Access'Object_Size: 64
AV1'Size: 64
Allocating AV1...
Allocating AV2...
AV1.all'Size: 8
As we've mentioned earlier on, Storage_Size
corresponds to the number of
storage elements reserved for an access type or a task object. In this case,
we see that the storage size of the UInt_7_Access
type is zero. This is
because we haven't indicated that memory should be reserved for this data type.
Thus, the compiler doesn't reserve memory and simply sets the size to zero.
Because Storage_Size
gives us the number of storage elements, we have
to multiply this value by System.Storage_Unit
to get the total
storage size in bits. (In this particular example, however, the multiplication
doesn't make any difference, as the number of storage elements is zero.)
Note that the size of our original data type UInt_7
is 7 bits, while the
size of its corresponding access type UInt_7_Access
(and the access
object AV1
) is 64 bits. This is due to the fact that the access type
doesn't contain an object, but rather memory information about an object. You
can retrieve the size of an object allocated via new
by first
dereferencing it — in our example, we do this by writing
AV1.all'Size
.
Now, let's use the Storage_Size
aspect to actually reserve memory for
this data type:
package Custom_Types is type UInt_7 is range 0 .. 127; type UInt_7_Reserved_Access is access UInt_7 with Storage_Size => 8; end Custom_Types;with Ada.Text_IO; use Ada.Text_IO; with System; with Custom_Types; use Custom_Types; procedure Show_Sizes is RAV1, RAV2 : UInt_7_Reserved_Access; begin Put_Line ("UInt_7_Reserved_Access'Storage_Size: " & UInt_7_Reserved_Access'Storage_Size'Image); Put_Line ("UInt_7_Reserved_Access'Storage_Size (bits): " & Integer'Image (UInt_7_Reserved_Access'Storage_Size * System.Storage_Unit)); Put_Line ("UInt_7_Reserved_Access'Size: " & UInt_7_Reserved_Access'Size'Image); Put_Line ("UInt_7_Reserved_Access'Object_Size: " & UInt_7_Reserved_Access'Object_Size'Image); Put_Line ("RAV1'Size: " & RAV1'Size'Image); New_Line; Put_Line ("Allocating RAV1..."); RAV1 := new UInt_7; Put_Line ("Allocating RAV2..."); RAV2 := new UInt_7; New_Line; end Show_Sizes;
Depending on your target architecture, you may see this output:
UInt_7_Reserved_Access'Storage_Size: 8
UInt_7_Reserved_Access'Storage_Size (bits): 64
UInt_7_Reserved_Access'Size: 64
UInt_7_Reserved_Access'Object_Size: 64
RAV1'Size: 64
Allocating RAV1...
Allocating RAV2...
raised STORAGE_ERROR : s-poosiz.adb:108 explicit raise
In this case, we're reserving 8 storage elements in the declaration of
UInt_7_Reserved_Access
.
type UInt_7_Reserved_Access is access UInt_7
with Storage_Size => 8;
Since each storage element corresponds to one byte (8 bits) in this
architecture, we're reserving a maximum of 64 bits (or 8 bytes) for the
UInt_7_Reserved_Access
type.
This example raises an exception at runtime — a storage error, to be more specific. This is because the maximum reserved size is 64 bits, and the size of a single access object is 64 bits as well. Therefore, after the first allocation, the reserved storage space is already consumed, so we cannot allocate a second access object.
This behavior might be quite limiting in many cases. However, for certain applications where memory is very constrained, this might be exactly what we want to see. For example, having an exception being raised when the allocated memory for this data type has reached its limit might allow the application to have enough memory to at least handle the exception gracefully.
Alignment
For many algorithms, it's important to ensure that we're using the appropriate
alignment. This can be done by using the Alignment
attribute and the
Alignment
aspect. Let's look at this example:
package Custom_Types is type UInt_7 is range 0 .. 127; type Aligned_UInt_7 is new UInt_7 with Alignment => 4; end Custom_Types;with Ada.Text_IO; use Ada.Text_IO; with Custom_Types; use Custom_Types; procedure Show_Alignment is V : constant UInt_7 := 0; Aligned_V : constant Aligned_UInt_7 := 0; begin Put_Line ("UInt_7'Alignment: " & UInt_7'Alignment'Image); Put_Line ("UInt_7'Size: " & UInt_7'Size'Image); Put_Line ("UInt_7'Object_Size: " & UInt_7'Object_Size'Image); Put_Line ("V'Alignment: " & V'Alignment'Image); Put_Line ("V'Size: " & V'Size'Image); New_Line; Put_Line ("Aligned_UInt_7'Alignment: " & Aligned_UInt_7'Alignment'Image); Put_Line ("Aligned_UInt_7'Size: " & Aligned_UInt_7'Size'Image); Put_Line ("Aligned_UInt_7'Object_Size: " & Aligned_UInt_7'Object_Size'Image); Put_Line ("Aligned_V'Alignment: " & Aligned_V'Alignment'Image); Put_Line ("Aligned_V'Size: " & Aligned_V'Size'Image); New_Line; end Show_Alignment;
Depending on your target architecture, you may see this output:
UInt_7'Alignment: 1
UInt_7'Size: 7
UInt_7'Object_Size: 8
V'Alignment: 1
V'Size: 8
Aligned_UInt_7'Alignment: 4
Aligned_UInt_7'Size: 7
Aligned_UInt_7'Object_Size: 32
Aligned_V'Alignment: 4
Aligned_V'Size: 32
In this example, we're reusing the UInt_7
type that we've already been
using in previous examples. Because we haven't specified any alignment for the
UInt_7
type, it has an alignment of 1 storage unit (or 8 bits). However,
in the declaration of the Aligned_UInt_7
type, we're using the
Alignment
aspect to request an alignment of 4 storage units (or 32
bits):
type Aligned_UInt_7 is new UInt_7
with Alignment => 4;
When using the Alignment
attribute for the Aligned_UInt_7
type,
we can confirm that its alignment is indeed 4 storage units (bytes).
Note that we can use the Alignment
attribute for both data types and
objects — in the code above, we're using UInt_7'Alignment
and
V'Alignment
, for example.
Because of the alignment we're specifying for the Aligned_UInt_7
type,
its size — indicated by the Object_Size
attribute — is 32
bits instead of 8 bits as for the UInt_7
type.
Note that you can also retrieve the alignment associated with a class using
S'Class'Alignment
. For example:
with Ada.Text_IO; use Ada.Text_IO; procedure Show_Class_Alignment is type Point_1D is tagged record X : Integer; end record; type Point_2D is new Point_1D with record Y : Integer; end record with Alignment => 16; type Point_3D is new Point_2D with record Z : Integer; end record; begin Put_Line ("1D_Point'Alignment: " & Point_1D'Alignment'Image); Put_Line ("1D_Point'Class'Alignment: " & Point_1D'Class'Alignment'Image); Put_Line ("2D_Point'Alignment: " & Point_2D'Alignment'Image); Put_Line ("2D_Point'Class'Alignment: " & Point_2D'Class'Alignment'Image); Put_Line ("3D_Point'Alignment: " & Point_3D'Alignment'Image); Put_Line ("3D_Point'Class'Alignment: " & Point_3D'Class'Alignment'Image); end Show_Class_Alignment;
Overlapping Storage
Algorithms can be designed to perform in-place or out-of-place processing. In other words, they can take advantage of the fact that input and output arrays share the same storage space or not.
We can use the Has_Same_Storage
and the Overlaps_Storage
attributes to retrieve more information about how the storage space of two
objects related to each other:
the
Has_Same_Storage
attribute indicates whether two objects have the exact same storage.A typical example is when both objects are exactly the same, so they obviously share the same storage. For example, for array
A
,A'Has_Same_Storage (A)
is alwaysTrue
.
the
Overlaps_Storage
attribute indicates whether two objects have at least one bit in common.Note that, if two objects have the same storage, this implies that their storage also overlaps. In other words,
A'Has_Same_Storage (B) = True
implies thatA'Overlaps_Storage (B) = True
.
Let's look at this example:
package Int_Array_Processing is type Int_Array is array (Positive range <>) of Integer; procedure Show_Storage (X : Int_Array; Y : Int_Array); procedure Process (X : Int_Array; Y : out Int_Array); end Int_Array_Processing;with Ada.Text_IO; use Ada.Text_IO; package body Int_Array_Processing is procedure Show_Storage (X : Int_Array; Y : Int_Array) is begin if X'Has_Same_Storage (Y) then Put_Line ("Info: X and Y have the same storage."); else Put_Line ("Info: X and Y don't have" & "the same storage."); end if; if X'Overlaps_Storage (Y) then Put_Line ("Info: X and Y overlap."); else Put_Line ("Info: X and Y don't overlap."); end if; end Show_Storage; procedure Process (X : Int_Array; Y : out Int_Array) is begin Put_Line ("==== PROCESS ===="); Show_Storage (X, Y); if X'Has_Same_Storage (Y) then Put_Line ("In-place processing..."); else if not X'Overlaps_Storage (Y) then Put_Line ("Out-of-place processing..."); else Put_Line ("Cannot process " & "overlapping arrays..."); end if; end if; New_Line; end Process; end Int_Array_Processing;with Int_Array_Processing; use Int_Array_Processing; procedure Main is A : Int_Array (1 .. 20) := (others => 3); B : Int_Array (1 .. 20) := (others => 4); begin Process (A, A); -- In-place processing: -- sharing the exact same storage Process (A (1 .. 10), A (10 .. 20)); -- Overlapping one component: A (10) Process (A (1 .. 10), A (11 .. 20)); -- Out-of-place processing: -- same array, but not sharing any storage Process (A, B); -- Out-of-place processing: -- two different arrays end Main;
In this code example, we implement two procedures:
Show_Storage
, which shows storage information about two arrays by using theHas_Same_Storage
andOverlaps_Storage
attributes.Process
, which are supposed to process an input arrayX
and store the processed data in the output arrayY
.Note that the implementation of this procedure is actually just a mock-up, so that no processing is actually taking place.
We have four different instances of how we can call the Process
procedure:
in the
Process (A, A)
call, we're using the same array for the input and output arrays. This is a perfect example of in-place processing. Because the input and the output arrays arguments are actually the same object, they obviously share the exact same storage.in the
Process (A (1 .. 10), A (10 .. 20))
call, we're using two slices of theA
array as input and output arguments. In this case, a single component of theA
array is shared:A (10)
. Because the storage space is overlapping, but not exactly the same, neither in-place nor out-of-place processing can usually be used in this case.in the
Process (A (1 .. 10), A (11 .. 20))
call, even though we're using the same arrayA
for the input and output arguments, we're using slices that are completely independent from each other, so that the input and output arrays are not sharing any storage in this case. Therefore, we can use out-of-place processing.in the
Process (A, B)
call, we have two different arrays — which obviously don't share any storage space —, so we can use out-of-place processing.
Packed Representation
As we've seen previously, the minimum number of bits required to represent a
data type might be less than the actual number of bits used to store an object
of that same type. We've seen an example where UInt_7'Size
was 7 bits,
while UInt_7'Object_Size
was 8 bits. The most extreme case is the one
for the Boolean
type: in this case, Boolean'Size
is 1 bit, while
Boolean'Object_Size
might be 8 bits (or even more on certain
architectures). In such cases, we have 7 (or more) unused bits in memory for
each object of Boolean
type. In other words, we're wasting memory. On
the other hand, we're gaining speed of access because we can directly access
each element without having to first change its internal representation back
and forth. We'll come back to this point later.
The situation is even worse when implementing bit-fields, which can be
declared as an array of Boolean
components. For example:
package Flag_Definitions is type Flags is array (Positive range <>) of Boolean; end Flag_Definitions;with Ada.Text_IO; use Ada.Text_IO; with Flag_Definitions; use Flag_Definitions; procedure Show_Flags is Flags_1 : Flags (1 .. 8); begin Put_Line ("Boolean'Size: " & Boolean'Size'Image); Put_Line ("Boolean'Object_Size: " & Boolean'Object_Size'Image); Put_Line ("Flags_1'Size: " & Flags_1'Size'Image); Put_Line ("Flags_1'Component_Size: " & Flags_1'Component_Size'Image); end Show_Flags;
Depending on your target architecture, you may see this output:
Boolean'Size: 1
Boolean'Object_Size: 8
Flags_1'Size: 64
Flags_1'Component_Size: 8
In this example, we're declaring the Flags
type as an array of
Boolean
components. As we can see in this case, although the size of the
Boolean
type is just 1 bit, an object of this type has a size of 8 bits.
Consequently, each component of the Flags
type has a size of 8 bits.
Moreover, an array with 8 components of Boolean
type — such as
the Flags_1
array — has a size of 64 bits.
Therefore, having a way to compact the representation — so that we can
store multiple objects without wasting storage space — may help us
improving memory usage. This is actually possible by using the Pack
aspect. For example, we could extend the previous example and declare a
Packed_Flags
type that makes use of this aspect:
package Flag_Definitions is type Flags is array (Positive range <>) of Boolean; type Packed_Flags is array (Positive range <>) of Boolean with Pack; end Flag_Definitions;with Ada.Text_IO; use Ada.Text_IO; with Flag_Definitions; use Flag_Definitions; procedure Show_Packed_Flags is Flags_1 : Flags (1 .. 8); Flags_2 : Packed_Flags (1 .. 8); begin Put_Line ("Boolean'Size: " & Boolean'Size'Image); Put_Line ("Boolean'Object_Size: " & Boolean'Object_Size'Image); Put_Line ("Flags_1'Size: " & Flags_1'Size'Image); Put_Line ("Flags_1'Component_Size: " & Flags_1'Component_Size'Image); Put_Line ("Flags_2'Size: " & Flags_2'Size'Image); Put_Line ("Flags_2'Component_Size: " & Flags_2'Component_Size'Image); end Show_Packed_Flags;
Depending on your target architecture, you may see this output:
Boolean'Size: 1
Boolean'Object_Size: 8
Flags_1'Size: 64
Flags_1'Component_Size: 8
Flags_2'Size: 8
Flags_2'Component_Size: 1
In this example, we're declaring the Flags_2
array of
Packed_Flags
type. Its size is 8 bits — instead of the 64 bits
required for the Flags_1
array. Because the array type
Packed_Flags
is packed, we can now effectively use this type to store an
object of Boolean
type using just 1 bit of the memory, as indicated by
the Flags_2'Component_Size
attribute.
In many cases, we need to convert between a normal representation (such as
the one used for the Flags_1
array above) to a packed representation
(such as the one for the Flags_2
array). In many programming languages,
this conversion may require writing custom code with manual bit-shifting and
bit-masking to get the proper target representation. In Ada, however, we just
need to indicate the actual type conversion, and the compiler takes care of
generating code containing bit-shifting and bit-masking to performs the type
conversion.
Let's modify the previous example and introduce this type conversion:
package Flag_Definitions is type Flags is array (Positive range <>) of Boolean; type Packed_Flags is array (Positive range <>) of Boolean with Pack; Default_Flags : constant Flags := (True, True, False, True, False, False, True, True); end Flag_Definitions;with Ada.Text_IO; use Ada.Text_IO; with Flag_Definitions; use Flag_Definitions; procedure Show_Flag_Conversion is Flags_1 : Flags (1 .. 8); Flags_2 : Packed_Flags (1 .. 8); begin Flags_1 := Default_Flags; Flags_2 := Packed_Flags (Flags_1); for I in Flags_2'Range loop Put_Line (I'Image & ": " & Flags_1 (I)'Image & ", " & Flags_2 (I)'Image); end loop; end Show_Flag_Conversion;
In this extended example, we're now declaring Default_Flags
as an array
of constant flags, which we use to initialize Flags_1
.
The actual conversion happens with Flags_2 := Packed_Flags (Flags_1)
.
Here, the type conversion Packed_Flags()
indicates that we're converting
from the normal representation (used for the Flags
type) to the packed
representation (used for Packed_Flags
type). We don't need to write more
code than that to perform the correct type conversion.
Also, by using the same strategy, we could read information from a packed representation. For example:
Flags_1 := Flags (Flags_2);
In this case, we use Flags()
to convert from a packed representation to
the normal representation.
We elaborate on the topic of converting between data representations in the section on changing data representation.
Trade-offs
As indicated previously, when we're using a packed representation (vs. using a standard unpacked representation), we're trading off speed of access for less memory consumption. The following table summarizes this:
Representation |
More speed of access |
Less memory consumption |
---|---|---|
Unpacked |
X |
|
Packed |
X |
On one hand, we have better memory usage when we apply packed representations because we may save many bits for each object. On the other hand, there's a cost associated with accessing those packed objects because they need to be unpacked before we can actually access them. In fact, the compiler generates code — using bit-shifting and bit-masking — that converts a packed representation into an unpacked representation, which we can then access. Also, when storing a packed object, the compiler generates code that converts the unpacked representation of the object into the packed representation.
This packing and unpacking mechanism has a performance cost associated with it, which results in less speed of access for packed objects. As usual in those circumstances, before using packed representation, we should assess whether memory constraints are more important than speed in our target architecture.
Record Representation and storage clauses
In this section, we discuss how to use record representation clauses to specify how a record is represented in memory. Our goal is to provide a brief introduction into the topic. If you're interested in more details, you can find a thorough discussion about record representation clauses in the Introduction to Embedded Systems Programming course.
Let's start with the simple approach of declaring a record type without providing further information. In this case, we're basically asking the compiler to select a reasonable representation for that record in the memory of our target architecture.
Let's see a simple example:
package P is type R is record A : Integer; B : Integer; end record; end P;
Considering a typical 64-bit PC architecture with 8-bit storage units, and
Integer
defined as a 32-bit type, we get this memory representation:
Each storage unit is a position in memory. In the graph above, the numbers on
the top (0, 1, 2, ...) represent those positions for record R
.
In addition, we can show the bits that are used for components A
and
B
:
The memory representation we see in the graph above can be described in Ada
using representation clauses, as you can see in the code starting at the
for R use record
line in the code example below — we'll discuss
the syntax and further details right after this example.
package P is type R is record A : Integer; B : Integer; end record; -- Representation clause for record R: for R use record A at 0 range 0 .. 31; -- ^ starting memory position B at 4 range 0 .. 31; -- ^ first bit .. last bit end record; end P;
Here, we're specifying that the A
component is stored in the bits #0 up
to #31 starting at position #0. Note that the position itself doesn't represent
an absolute address in the device's memory; instead, it's relative to the
memory space reserved for that record. The B
component has the same
32-bit range, but starts at position #4.
This is a generalized view of the syntax:
for Record_Type use record
Component_Name at Start_Position
range First_Bit .. Last_Bit;
end record;
These are the elements we see above:
Component_Name
: name of the component (from the record type declaration);Start_Position
: start position — in storage units — of the memory space reserved for that component;First_Bit
: first bit (in the start position) of the component;Last_Bit
: last bit of the component.
Note that the last bit of a component might be in a different storage unit.
Since the Integer
type has a larger width (32 bits) than the storage
unit (8 bits), components of that type span over multiple storage units.
Therefore, in our example, the first bit of component A
is at position
#0, while the last bit is at position #3.
Also note that the last eight bits of component A
are bits #24 .. #31.
If we think in terms of storage units, this corresponds to bits #0 .. #7 of
position #3. However, when specifying the last bit in Ada, we always use the
First_Bit
value as a reference, not the position where those bits might
end up. Therefore, we write range 0 .. 31
, well knowing that those 32
bits span over four storage units (positions #0 .. #3).
In the Ada Reference Manual
Storage Place Attributes
We can retrieve information about the start position, and the first and last bits of a component by using the storage place attributes:
Position
, which retrieves the start position of a component;First_Bit
, which retrieves the first bit of a component;Last_Bit
, which retrieves the last bit of a component.
Note, however, that these attributes can only be used with actual records, and not with record types.
We can revisit the previous example and verify how the compiler represents the
R
type in memory:
package P is type R is record A : Integer; B : Integer; end record; end P;with Ada.Text_IO; use Ada.Text_IO; with System; with P; use P; procedure Show_Storage is R1 : R; begin Put_Line ("R'Size: " & R'Size'Image); Put_Line ("R'Object_Size: " & R'Object_Size'Image); New_Line; Put_Line ("System.Storage_Unit: " & System.Storage_Unit'Image); New_Line; Put_Line ("R1.A'Position : " & R1.A'Position'Image); Put_Line ("R1.A'First_Bit : " & R1.A'First_Bit'Image); Put_Line ("R1.A'Last_Bit : " & R1.A'Last_Bit'Image); New_Line; Put_Line ("R1.B'Position : " & R1.B'Position'Image); Put_Line ("R1.B'First_Bit : " & R1.B'First_Bit'Image); Put_Line ("R1.B'Last_Bit : " & R1.B'Last_Bit'Image); end Show_Storage;
On a typical 64-bit PC architecture, you probably see this output:
R'Size: 64
R'Object_Size: 64
System.Storage_Unit: 8
R1.A'Position : 0
R1.A'First_Bit : 0
R1.A'Last_Bit : 31
R1.B'Position : 4
R1.B'First_Bit : 0
R1.B'Last_Bit : 31
First of all, we see that the size of the R
type is 64 bits, which can
be explained by those two 32-bit integer components. Then, we see that
components A
and B
start at positions #0 and #4, and each one
makes use of bits in the range from #0 to #31. This matches the graph we've
seen above.
In the Ada Reference Manual
Using Representation Clauses
We can use representation clauses to change the way the compiler handles
memory for a record type. For example, let's say we want to have an empty
storage unit between components A
and B
. We can use a
representation clause where we specify that component B
starts at
position #5 instead of #4, leaving an empty byte after component A
and
before component B
:
This is the code that implements that:
package P is type R is record A : Integer; B : Integer; end record; for R use record A at 0 range 0 .. 31; B at 5 range 0 .. 31; end record; end P;with Ada.Text_IO; use Ada.Text_IO; with P; use P; procedure Show_Empty_Byte is begin Put_Line ("R'Size: " & R'Size'Image); Put_Line ("R'Object_Size: " & R'Object_Size'Image); end Show_Empty_Byte;
When running the application above, we see that, due to the extra byte in the
record representation, the sizes increase. On a typical 64-bit PC,
R'Size
is now 76 bits, which reflects the additional eight bits that we
introduced between components A
and B
. Depending on the target
architecture, you may also see that R'Object_Size
is now 96 bits, which
is the size the compiler selects as the most appropriate for this record type.
As we've mentioned in the previous section, we can use aspects to request a
specific size to the compiler. In this case, we could use the
Object_Size
aspect:
package P is type R is record A : Integer; B : Integer; end record with Object_Size => 72; for R use record A at 0 range 0 .. 31; B at 5 range 0 .. 31; end record; end P;with Ada.Text_IO; use Ada.Text_IO; with P; use P; procedure Show_Empty_Byte is begin Put_Line ("R'Size: " & R'Size'Image); Put_Line ("R'Object_Size: " & R'Object_Size'Image); end Show_Empty_Byte;
If the code compiles, R'Size
and R'Object_Size
should now have
the same value.
Derived Types And Representation Clauses
In some cases, you might want to modify the memory representation of a record without impacting existing code. For example, you might want to use a record type that was declared in a package that you're not allowed to change. Also, you would like to modify its memory representation in your application. A nice strategy is to derive a type and use a representation clause for the derived type.
We can apply this strategy on our previous example. Let's say we would like to
use record type R
from package P
in our application, but we're
not allowed to modify package P
— or the record type, for that
matter. In this case, we could simply derive R
as R_New
and use a
representation clause for R_New
. This is exactly what we do in the
specification of the child package P.Rep
:
package P is type R is record A : Integer; B : Integer; end record; end P;package P.Rep is type R_New is new R with Object_Size => 72; for R_New use record A at 0 range 0 .. 31; B at 5 range 0 .. 31; end record; end P.Rep;with Ada.Text_IO; use Ada.Text_IO; with P; use P; with P.Rep; use P.Rep; procedure Show_Empty_Byte is begin Put_Line ("R'Size: " & R'Size'Image); Put_Line ("R'Object_Size: " & R'Object_Size'Image); Put_Line ("R_New'Size: " & R_New'Size'Image); Put_Line ("R_New'Object_Size: " & R_New'Object_Size'Image); end Show_Empty_Byte;
When running this example, we see that the R
type retains the memory
representation selected by the compiler for the target architecture, while the
R_New
has the memory representation that we specified.
Representation on Bit Level
A very common application of representation clauses is to specify individual bits of a record. This is particularly useful, for example, when mapping registers or implementing protocols.
Let's consider the following fictitious register as an example:
Here, S
is the current status, Error
is a flag, and V1
contains a value. Due to the fact that we can use representation clauses to
describe individual bits of a register as records, the implementation becomes
as simple as this:
package P is type Status is (Ready, Waiting, Processing, Done); type UInt_3 is range 0 .. 2 ** 3 - 1; type Simple_Reg is record S : Status; Error : Boolean; V1 : UInt_3; end record; for Simple_Reg use record S at 0 range 0 .. 1; -- Bit #2 and 3: reserved! Error at 0 range 4 .. 4; V1 at 0 range 5 .. 7; end record; end P;with Ada.Text_IO; use Ada.Text_IO; with P; use P; procedure Show_Simple_Reg is begin Put_Line ("Simple_Reg'Size: " & Simple_Reg'Size'Image); Put_Line ("Simple_Reg'Object_Size: " & Simple_Reg'Object_Size'Image); end Show_Simple_Reg;
As we can see in the declaration of the Simple_Reg
type, each component
represents a field from our register, and it has a fixed location (which
matches the register representation we see in the graph above). Any operation
on the register is as simple as accessing the record component. For example:
with Ada.Text_IO; use Ada.Text_IO; with P; use P; procedure Show_Simple_Reg is Default : constant Simple_Reg := (S => Ready, Error => False, V1 => 0); R : Simple_Reg := Default; begin Put_Line ("R.S: " & R.S'Image); R.V1 := 4; Put_Line ("R.V1: " & R.V1'Image); end Show_Simple_Reg;
As we can see in the example, to retrieve the current status of the register,
we just have to write R.S
. To update the V1 field of the register with
the value 4, we just have to write R.V1 := 4
. No extra code —
such as bit-masking or bit-shifting — is needed here.
In other languages
Some programming languages require that developers use complicated, error-prone approaches — which may include manually bit-shifting and bit-masking variables — to retrieve information from or store information to individual bits or registers. In Ada, however, this is efficiently handled by the compiler, so that developers only need to correctly describe the register mapping using representation clauses.
Changing Data Representation
Note
This section was originally written by Robert Dewar and published as Gem #27: Changing Data Representation and Gem #28.
A powerful feature of Ada is the ability to specify the exact data layout. This is particularly important when you have an external device or program that requires a very specific format. Some examples are:
package Communication is type Com_Packet is record Key : Boolean; Id : Character; Val : Integer range 100 .. 227; end record; for Com_Packet use record Key at 0 range 0 .. 0; Id at 0 range 1 .. 8; Val at 0 range 9 .. 15; end record; end Communication;
which lays out the fields of a record, and in the case of Val
, forces a
biased representation in which all zero bits represents 100. Another example
is:
package Array_Representation is type Val is (A, B, C, D, E, F, G, H); type Arr is array (1 .. 16) of Val with Component_Size => 3; end Array_Representation;
which forces the components to take only 3 bits, crossing byte boundaries as needed. A final example is:
package Enumeration_Representation is type Status is (Off, On, Unknown); for Status use (Off => 2#001#, On => 2#010#, Unknown => 2#100#); end Enumeration_Representation;
which allows specified values for an enumeration type, instead of the efficient default values of 0, 1, 2.
In all these cases, we might use these representation clauses to match external specifications, which can be very useful. The disadvantage of such layouts is that they are inefficient, and accessing individual components, or, in the case of the enumeration type, looping through the values can increase space and time requirements for the program code.
One approach that is often effective is to read or write the data in question in this specified form, but internally in the program represent the data in the normal default layout, allowing efficient access, and do all internal computations with this more efficient form.
To follow this approach, you will need to convert between the efficient format and the specified format. Ada provides a very convenient method for doing this, as described in RM 13.6 "Change of Representation".
The idea is to use type derivation, where one type has the specified format and the other has the normal default format. For instance for the array case above, we would write:
package Array_Representation is type Val is (A, B, C, D, E, F, G, H); type Arr is array (1 .. 16) of Val; type External_Arr is new Arr with Component_Size => 3; end Array_Representation;
Now we read and write the data using the External_Arr
type. When we want
to convert to the efficient form, Arr
, we simply use a type conversion.
with Array_Representation; use Array_Representation; procedure Using_Array_For_IO is Input_Data : External_Arr; Work_Data : Arr; Output_Data : External_Arr; begin -- (read data into Input_Data) -- Now convert to internal form Work_Data := Arr (Input_Data); -- (computations using efficient -- Work_Data form) -- Convert back to external form Output_Data := External_Arr (Work_Data); end Using_Array_For_IO;
Using this approach, the quite complex task of copying all the data of the array from one form to another, with all the necessary masking and shift operations, is completely automatic.
Similar code can be used in the record and enumeration type cases. It is even possible to specify two different representations for the two types, and convert from one form to the other, as in:
package Enumeration_Representation is type Status_In is (Off, On, Unknown); type Status_Out is new Status_In; for Status_In use (Off => 2#001#, On => 2#010#, Unknown => 2#100#); for Status_Out use (Off => 103, On => 1045, Unknown => 7700); end Enumeration_Representation;
There are two restrictions that must be kept in mind when using this feature. First, you have to use a derived type. You can't put representation clauses on subtypes, which means that the conversion must always be explicit. Second, there is a rule RM 13.1 (10) that restricts the placement of interesting representation clauses:
10 For an untagged derived type, no type-related representation items are allowed if the parent type is a by-reference type, or has any user-defined primitive subprograms.
All the representation clauses that are interesting from the point of view of change of representation are "type related", so for example, the following sequence would be illegal:
package Array_Representation is type Val is (A, B, C, D, E, F, G, H); type Arr is array (1 .. 16) of Val; procedure Rearrange (Arg : in out Arr); type External_Arr is new Arr with Component_Size => 3; end Array_Representation;
Why these restrictions? Well, the answer is a little complex, and has to do with efficiency considerations, which we will address below.
Restrictions
In the previous subsection, we discussed the use of derived types and representation clauses to achieve automatic change of representation. More accurately, this feature is not completely automatic, since it requires you to write an explicit conversion. In fact there is a principle behind the design here which says that a change of representation should never occur implicitly behind the back of the programmer without such an explicit request by means of a type conversion.
The reason for that is that the change of representation operation can be very expensive, since in general it can require component by component copying, changing the representation on each component.
Let's have a look at the -gnatG
expanded code to see what is hidden under
the covers here. For example, the conversion Arr (Input_Data)
from the
previous example generates the following expanded code:
B26b : declare
[subtype p__TarrD1 is integer range 1 .. 16]
R25b : p__TarrD1 := 1;
begin
for L24b in 1 .. 16 loop
[subtype p__arr___XP3 is
system__unsigned_types__long_long_unsigned range 0 ..
16#FFFF_FFFF_FFFF#]
work_data := p__arr___XP3!((work_data and not shift_left!(
16#7#, 3 * (integer(L24b - 1)))) or shift_left!(p__arr___XP3!
(input_data (R25b)), 3 * (integer(L24b - 1))));
R25b := p__TarrD1'succ(R25b);
end loop;
end B26b;
That's pretty horrible! In fact, we could have simplified it for this section, but we have left it in its original form, so that you can see why it is nice to let the compiler generate all this stuff so you don't have to worry about it yourself.
Given that the conversion can be pretty inefficient, you don't want to convert backwards and forwards more than you have to, and the whole approach is only worthwhile if we'll be doing extensive computations involving the value.
The expense of the conversion explains two aspects of this feature that are not obvious. First, why do we require derived types instead of just allowing subtypes to have different representations, avoiding the need for an explicit conversion?
The answer is precisely that the conversions are expensive, and you don't want them happening behind your back. So if you write the explicit conversion, you get all the gobbledygook listed above, but you can be sure that this never happens unless you explicitly ask for it.
This also explains the restriction we mentioned in previous subsection from RM 13.1 (10):
10 For an untagged derived type, no type-related representation items are allowed if the parent type is a by-reference type, or has any user-defined primitive subprograms.
It turns out this restriction is all about avoiding implicit changes of representation. Let's have a look at how type derivation works when there are primitive subprograms defined at the point of derivation. Consider this example:
package My_Ints is type My_Int_1 is range 1 .. 10; function Odd (Arg : My_Int_1) return Boolean; type My_Int_2 is new My_Int_1; end My_Ints;package body My_Ints is function Odd (Arg : My_Int_1) return Boolean is (True); -- Dummy implementation! end My_Ints;
Now when we do the type derivation, we inherit the function Odd
for
My_Int_2
. But where does this function come from? We haven't
written it explicitly, so the compiler somehow materializes this new implicit
function. How does it do that?
We might think that a complete new function is created including a body in
which My_Int_2
replaces My_Int_1
, but that would be impractical
and expensive. The actual mechanism avoids the need to do this by use of
implicit type conversions. Suppose after the above declarations, we write:
with My_Ints; use My_Ints; procedure Using_My_Int is Var : My_Int_2; begin if Odd (Var) then -- ^ Calling Odd function -- for My_Int_2 type. null; end if; end Using_My_Int;
The compiler translates this as:
with My_Ints; use My_Ints; procedure Using_My_Int is Var : My_Int_2; begin if Odd (My_Int_1 (Var)) then -- ^ Converting My_Int_2 to -- My_Int_1 type before -- calling Odd function. null; end if; end Using_My_Int;
This implicit conversion is a nice trick, it means that we can get the effect
of inheriting a new operation without actually having to create it.
Furthermore, in a case like this, the type conversion generates no code,
since My_Int_1
and My_Int_2
have the same representation.
But the whole point is that they might not have the same representation if one
of them had a representation clause that made the representations different,
and in this case the implicit conversion inserted by the compiler could be
expensive, perhaps generating the junk we quoted above for the Arr
case.
Since we never want that to happen implicitly, there is a rule to prevent it.
The business of forbidding by-reference types (which includes all tagged types) is also driven by this consideration. If the representations are the same, it is fine to pass by reference, even in the presence of the conversion, but if there was a change of representation, it would force a copy, which would violate the by-reference requirement.
So to summarize this section, on the one hand Ada gives you a very convenient way to trigger these complex conversions between different representations. On the other hand, Ada guarantees that you never get these potentially expensive conversions happening unless you explicitly ask for them.
Valid Attribute
When receiving data from external sources, we're subjected to problems such as transmission errors. If not handled properly, erroneous data can lead to major issues in an application.
One of those issues originates from the fact that transmission errors might lead to invalid information stored in memory. When proper checks are active, using invalid information is detected at runtime and an exception is raised at this point, which might then be handled by the application.
Instead of relying on exception handling, however, we could instead ensure that
the information we're about to use is valid. We can do this by using the
Valid
attribute. For example, if we have a variable Var
, we can
verify that the value stored in Var
is valid by writing
Var'Valid
, which returns a Boolean
value. Therefore, if the value
of Var
isn't valid, Var'Valid
returns False
, so we can
have code that handles this situation before we actually make use of
Var
. In other words, instead of handling a potential exception in other
parts of the application, we can proactively verify that input information is
correct and avoid that an exception is raised.
In the next example, we show an application that
generates a file containing mock-up data, and then
reads information from this file as state values.
The mock-up data includes valid and invalid states.
procedure Create_Test_File (File_Name : String);with Ada.Sequential_IO; procedure Create_Test_File (File_Name : String) is package Integer_Sequential_IO is new Ada.Sequential_IO (Integer); use Integer_Sequential_IO; F : File_Type; begin Create (F, Out_File, File_Name); Write (F, 1); Write (F, 2); Write (F, 4); Write (F, 3); Write (F, 2); Write (F, 10); Close (F); end Create_Test_File;with Ada.Sequential_IO; package States is type State is (Off, On, Waiting) with Size => Integer'Size; for State use (Off => 1, On => 2, Waiting => 4); package State_Sequential_IO is new Ada.Sequential_IO (State); procedure Read_Display_States (File_Name : String); end States;with Ada.Text_IO; use Ada.Text_IO; package body States is procedure Read_Display_States (File_Name : String) is use State_Sequential_IO; F : State_Sequential_IO.File_Type; S : State; procedure Display_State (S : State) is begin -- Before displaying the value, -- check whether it's valid or not. if S'Valid then Put_Line (S'Image); else Put_Line ("Invalid value detected!"); end if; end Display_State; begin Open (F, In_File, File_Name); while not End_Of_File (F) loop Read (F, S); Display_State (S); end loop; Close (F); end Read_Display_States; end States;with States; use States; with Create_Test_File; procedure Show_States_From_File is File_Name : constant String := "data.bin"; begin Create_Test_File (File_Name); Read_Display_States (File_Name); end Show_States_From_File;
When running the application, you'd see this output:
OFF
ON
WAITING
Invalid value detected!
ON
Invalid value detected!
Let's start our discussion on this example with the States
package,
which contains the declaration of the State
type. This type is a simple
enumeration containing three states: Off
, On
and Waiting
.
We're assigning specific integer values for this type by declaring an
enumeration representation clause. Note that we're using the Size
aspect
to request that objects of this type have the same size as the Integer
type. This becomes important later on when parsing data from the file.
In the Create_Test_File
procedure, we create a file containing integer
values, which is parsed later by the Read_Display_States
procedure. The
Create_Test_File
procedure doesn't contain any reference to the
State
type, so we're not constrained to just writing information that is
valid for this type. On the contrary, this procedure makes use of the
Integer
type, so we can write any integer value to the file. We use this
strategy to write both valid and invalid values of State
to the file.
This allows us to simulate an environment where transmission errors occur.
We call the Read_Display_States
procedure to read information from the
file and display each state stored in the file. In the main loop of this
procedure, we call Read
to read a state from the file and store it in
the S
variable. We then call the nested Display_State
procedure
to display the actual state stored in S
. The most important line of code
in the Display_State
procedure is the one that uses the Valid
attribute:
if S'Valid then
In this line, we're verifying that the S
variable contains a valid state
before displaying the actual information from S
. If the value stored in
S
isn't valid, we can handle the issue accordingly. In this case, we're
simply displaying a message indicating that an invalid value was detected. If
we didn't have this check, the Constraint_Error
exception would be
raised when trying to use invalid data stored in S
— this would
happen, for example, after reading the integer value 3 from the input file.
In summary, using the Valid
attribute is a good strategy we can employ
when we know that information stored in memory might be corrupted.
In the Ada Reference Manual
Unchecked Union
We've introduced variant records back in the
Introduction to Ada course.
In simple terms, a variant record is a record with discriminants that allows
for changing its structure. Basically, it's a record containing a case
.
The State_Or_Integer
declaration in the States
package below is
an example of a variant record:
package States is type State is (Off, On, Waiting) with Size => Integer'Size; for State use (Off => 1, On => 2, Waiting => 4); type State_Or_Integer (Use_Enum : Boolean) is record case Use_Enum is when False => I : Integer; when True => S : State; end case; end record; procedure Display_State_Value (V : State_Or_Integer); end States;with Ada.Text_IO; use Ada.Text_IO; package body States is procedure Display_State_Value (V : State_Or_Integer) is begin Put_Line ("State: " & V.S'Image); Put_Line ("Value: " & V.I'Image); end Display_State_Value; end States;
As mentioned in the previous course, if you try to access a component that is
not valid for your record, a Constraint_Error
exception is raised. For
example, in the implementation of the Display_State_Value
procedure,
we're trying to retrieve the value of the integer component (I
) of the
V
record. When calling this procedure, the Constraint_Error
exception is raised as expected because Use_Enum
is set to True
,
so that the I
component is invalid — only the S
component
is valid in this case.
with States; use States; procedure Show_Variant_Rec_Error is V : State_Or_Integer (Use_Enum => True); begin V.S := On; Display_State_Value (V); end Show_Variant_Rec_Error;
In addition to not being able to read the value of a component that isn't
valid, assigning a value to a component that isn't valid also raises an
exception at runtime. In this example, we cannot assign to V.I
:
with States; use States; procedure Show_Variant_Rec_Error is V : State_Or_Integer (Use_Enum => True); begin V.I := 4; -- Error: V.I cannot be accessed because -- Use_Enum is set to True. end Show_Variant_Rec_Error;
We may circumvent this limitation by using the Unchecked_Union
aspect.
For example, we can derive a new type from State_Or_Integer
and use
this aspect in its declaration. We do this in the declaration of the
Unchecked_State_Or_Integer
type below.
package States is type State is (Off, On, Waiting) with Size => Integer'Size; for State use (Off => 1, On => 2, Waiting => 4); type State_Or_Integer (Use_Enum : Boolean) is record case Use_Enum is when False => I : Integer; when True => S : State; end case; end record; type Unchecked_State_Or_Integer (Use_Enum : Boolean) is new State_Or_Integer (Use_Enum) with Unchecked_Union; procedure Display_State_Value (V : Unchecked_State_Or_Integer); end States;with Ada.Text_IO; use Ada.Text_IO; package body States is procedure Display_State_Value (V : Unchecked_State_Or_Integer) is begin Put_Line ("State: " & V.S'Image); Put_Line ("Value: " & V.I'Image); end Display_State_Value; end States;
Because we now use the Unchecked_State_Or_Integer
type for the input
parameter of the Display_State_Value
procedure, no exception is raised
at runtime, as both components are now accessible. For example:
with States; use States; procedure Show_Unchecked_Union is V : State_Or_Integer (Use_Enum => True); begin V.S := On; Display_State_Value (Unchecked_State_Or_Integer (V)); end Show_Unchecked_Union;
Note that, in the call to the Display_State_Value
procedure, we first
need to convert the V
argument from the State_Or_Integer
to the
Unchecked_State_Or_Integer
type.
Also, we can assign to any of the components of a record that has the
Unchecked_Union
aspect. In our example, we can now assign to both the
S
and the I
components of the V
record:
with States; use States; procedure Show_Unchecked_Union is V : Unchecked_State_Or_Integer (Use_Enum => True); begin V := (Use_Enum => True, S => On); Display_State_Value (V); V := (Use_Enum => False, I => 4); Display_State_Value (V); end Show_Unchecked_Union;
In the example above, we're use an aggregate in the assignments to V
. By
doing so, we avoid that Use_Enum
is set to the wrong component. For
example:
with States; use States; procedure Show_Unchecked_Union is V : Unchecked_State_Or_Integer (Use_Enum => True); begin V.S := On; Display_State_Value (V); V.I := 4; -- Error: cannot directly assign to V.I, -- as Use_Enum is set to True. Display_State_Value (V); end Show_Unchecked_Union;
Here, even though the record has the Unchecked_Union
attribute, we
cannot directly assign to the I
component because Use_Enum
is set
to True
, so only the S
is accessible. We can, however, read its
value, as we do in the Display_State_Value
procedure.
Be aware that, due to the fact the union is not checked, we might write invalid
data to the record. In the example below, we initialize the I
component
with 3, which is a valid integer value, but results in an invalid value for
the S
component, as the value 3 cannot be mapped to the representation
of the State
type.
with States; use States; procedure Show_Unchecked_Union is V : Unchecked_State_Or_Integer (Use_Enum => True); begin V := (Use_Enum => False, I => 3); Display_State_Value (V); end Show_Unchecked_Union;
To mitigate this problem, we could use the Valid
attribute —
discussed in the previous section — for the S
component before
trying to use its value in the implementation of the Display_State_Value
procedure:
with Ada.Text_IO; use Ada.Text_IO; package body States is procedure Display_State_Value (V : Unchecked_State_Or_Integer) is begin if V.S'Valid then Put_Line ("State: " & V.S'Image); else Put_Line ("State: <invalid>"); end if; Put_Line ("Value: " & V.I'Image); end Display_State_Value; end States;with States; use States; procedure Show_Unchecked_Union is V : Unchecked_State_Or_Integer (Use_Enum => True); begin V := (Use_Enum => False, I => 3); Display_State_Value (V); end Show_Unchecked_Union;
However, in general, you should avoid using the Unchecked_Union
aspect
due to the potential issues you might introduce into your application. In the
majority of the cases, you don't need it at all — except for special
cases such as when interfacing with C code that makes use of union types or
solving very specific problems when doing low-level programming.
In the Ada Reference Manual
Addresses
In other languages, such as C, the concept of pointers and addresses plays a prominent role. (In fact, in C, many optimizations rely on the usage of pointer arithmetic.) The concept of addresses does exist in Ada, but it's mainly reserved for very specific applications, mostly related to low-level programming. In general, other approaches — such as using access types — are more than sufficient. (We discuss access types in another chapter. Also, later on in that chapter, we discuss the relation between access types and addresses.) In this section, we discuss some details about using addresses in Ada.
We make use of the Address
type, which is defined in the System
package, to handle addresses. In contrast to other programming languages (such
as C or C++), an address in Ada isn't an integer value: its definition depends
on the compiler implementation, and it's actually driven directly by the
hardware. For now, let's consider it to usually be a private type — this
can be seen as an attempt to achieve application code portability, given the
variations in hardware that result in different definitions of what an address
actually is.
The Address
type has support for
address comparison and
address arithmetic (also
known as pointer arithmetic in C). We discuss these topics later in this
section. First, let's talk about the Address
attribute and the
Address
aspect.
In the Ada Reference Manual
Address attribute
The Address
attribute allows us to get the address of an object.
For example:
with System; use System; procedure Use_Address is I : aliased Integer := 5; A : Address; begin A := I'Address; end Use_Address;
Here, we're assigning the address of the I
object to the A
address.
In the GNAT toolchain
GNAT offers a very useful extension to the System
package to
retrieve a string for an address: System.Address_Image
. This is the
function profile:
function System.Address_Image
(A : System.Address) return String;
We can use this function to display the address in an user message, for example:
with Ada.Text_IO; use Ada.Text_IO; with System.Address_Image; procedure Show_Address_Attribute is I : aliased Integer := 5; begin Put_Line ("Address : " & System.Address_Image (I'Address)); end Show_Address_Attribute;
In the Ada Reference Manual
Address aspect
Usually, we let the compiler select the address of an object in memory, or let
it use a register to store that object. However, we can specify the address of
an object with the Address
aspect. In this case, the compiler won't
select an address automatically, but use the address that we're specifying. For
example:
with System; use System; with System.Address_Image; with Ada.Text_IO; use Ada.Text_IO; procedure Show_Address is I_Main : aliased Integer; I_Mapped : Integer with Address => I_Main'Address; begin Put_Line ("I_Main'Address : " & System.Address_Image (I_Main'Address)); Put_Line ("I_Mapped'Address : " & System.Address_Image (I_Mapped'Address)); end Show_Address;
This approach allows us to create an overlay. For example:
with Ada.Text_IO; use Ada.Text_IO; procedure Simple_Overlay is type State is (Off, State_1, State_2) with Size => Integer'Size; for State use (Off => 0, State_1 => 32, State_2 => 64); S : State; I : Integer with Address => S'Address, Import, Volatile; begin S := State_2; Put_Line ("I = " & Integer'Image (I)); end Simple_Overlay;---- run info:
Here, I
is an overlay of S
, as it uses S'Address
. With
this approach, we can either use the enumeration directly (by using the
S
object of State
type) or its integer representation (by using
the I
variable).
In the GNAT toolchain
We could call the GNAT-specific System'To_Address
attribute when using
the Address
aspect, as we did while talking about the
Atomic aspect:
with System; package Shared_Var_Types is private R : Integer with Atomic, Address => System'To_Address (16#FFFF00A0#); end Shared_Var_Types;
In this case, R
will refer to the address in memory that we're
specifying (16#FFFF00A0#
in this case).
As explained in the
GNAT Reference Manual,
the System'To_Address
attribute denotes a function identical to
To_Address
(from the System.Storage_Elements
package) except
that it is a static attribute. (We talk about the
To_Address function function later on.)
In the Ada Reference Manual
Address comparison
We can compare addresses using the common comparison operators. For example:
with System; use System; with System.Address_Image; with Ada.Text_IO; use Ada.Text_IO; procedure Show_Address is I, J : Integer; begin Put_Line ("I'Address : " & System.Address_Image (I'Address)); Put_Line ("J'Address : " & System.Address_Image (J'Address)); if I'Address = J'Address then Put_Line ("I'Address = J'Address"); elsif I'Address < J'Address then Put_Line ("I'Address < J'Address"); else Put_Line ("I'Address > J'Address"); end if; end Show_Address;
In this example, we compare the address of the I
object with the address
of the J
object using the =
, <
and >
operators.
In the Ada Reference Manual
Address to integer conversion
The System.Storage_Elements
package offers an integer representation of
an address via the Integer_Address
type, which is an integer type
unrelated to common integer types such as Integer
and
Long_Integer
. (The actual definition of Integer_Address
is
compiler-dependent, and it can be a signed or modular integer subtype.)
We can convert between the Address
and Integer_Address
types by
using the To_Address
and To_Integer
functions. Let's see an
example:
with System; use System; with System.Storage_Elements; use System.Storage_Elements; with System.Address_Image; with Ada.Text_IO; use Ada.Text_IO; procedure Show_Address is I : Integer; A1, A2 : Address; IA : Integer_Address; begin A1 := I'Address; IA := To_Integer (A1); A2 := To_Address (IA); Put_Line ("A1 : " & System.Address_Image (A1)); Put_Line ("IA : " & Integer_Address'Image (IA)); Put_Line ("A2 : " & System.Address_Image (A2)); end Show_Address;
Here, we retrieve the address of the I
object and store it in the
A1
address. Then, we convert A1
to an integer address by calling
To_Integer
(and store it in IA
). Finally, we convert this
integer address back to an actual address by calling To_Address
.
In the Ada Reference Manual
Address arithmetic
Although Ada supports address arithmetic, which we discuss in this section, it should be reserved for very specific applications such as low-level programming. However, even in situations that require close access to the underlying hardware, using address arithmetic might not be the approach you should consider — make sure to evaluate other options first!
Ada supports address arithmetic via the System.Storage_Elements
package,
which includes operators such as +
and -
for addresses. Let's see
a code example where we iterate over an array by incrementing an address that
points to each component in memory:
with System; use System; with System.Storage_Elements; use System.Storage_Elements; with System.Address_Image; with Ada.Text_IO; use Ada.Text_IO; procedure Show_Address is Arr : array (1 .. 10) of Integer; A : Address := Arr'Address; -- ^^^^^^^^^^^ -- Initializing address object with -- address of the first component of Arr. -- -- We could write this as well: -- ___ := Arr (1)'Address begin for I in Arr'Range loop declare Curr : Integer with Address => A; begin Curr := I; Put_Line ("Curr'Address : " & System.Address_Image (Curr'Address)); end; -- -- Address arithmetic -- A := A + Storage_Offset (Integer'Size) / Storage_Unit; -- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -- Moving to next component end loop; for I in Arr'Range loop Put_Line ("Arr (" & Integer'Image (I) & ") :" & Integer'Image (Arr (I))); end loop; end Show_Address;
In this example, we initialize the address A
by retrieving the address
of the first component of the array Arr
. (Note that we could have
written Arr(1)'Address
instead of Arr'Address
. In any
case, the language guarantees that Arr'Address
gives us the address of
the first component, i.e. Arr'Address = Arr(1)'Address
.)
Then, in the loop, we declare
an overlay Curr
using the current value of the A
address. We can
then operate on this overlay — here, we assign I
to Curr
.
Finally, in the loop, we increment address A
and make it point to the
next component in the Arr
array — to do so, we calculate the size
of an Integer
component in storage units. (For details on storage units,
see the section on
storage size attribute.)
In other languages
The code example above corresponds (more or less) to the following C code:
#include <stdio.h> int main(int argc, const char * argv[]) { int i; int arr[10]; int *a = arr; /* int *a = &arr[0]; */ for (i = 0; i < 10; i++) { *a++ = i; printf("curr address: %p\n", a); } for (i = 0; i < 10; i++) { printf("arr[%d]: %d\n", i, arr[i]); } return 0; }
While pointer arithmetic is very common in C, using address arithmetic in Ada is far from common, and it should be only used when it's really necessary to do so.
In the Ada Reference Manual
Discarding names
As we know, we can use the Image
attribute of a type to get a string
associated with this type. This is useful for example when we want to display a
user message for an enumeration type:
with Ada.Text_IO; use Ada.Text_IO; procedure Show_Enumeration_Image is type Months is (January, February, March, April, May, June, July, August, September, October, November, December); M : constant Months := January; begin Put_Line ("Month: " & Months'Image (M)); end Show_Enumeration_Image;
This is similar to having this code:
with Ada.Text_IO; use Ada.Text_IO; procedure Show_Enumeration_Image is type Months is (January, February, March, April, May, June, July, August, September, October, November, December); M : constant Months := January; function Months_Image (M : Months) return String is begin case M is when January => return "JANUARY"; when February => return "FEBRUARY"; when March => return "MARCH"; when April => return "APRIL"; when May => return "MAY"; when June => return "JUNE"; when July => return "JULY"; when August => return "AUGUST"; when September => return "SEPTEMBER"; when October => return "OCTOBER"; when November => return "NOVEMBER"; when December => return "DECEMBER"; end case; end Months_Image; begin Put_Line ("Month: " & Months_Image (M)); end Show_Enumeration_Image;
Here, the Months_Image
function associates a string with each month of
the Months
enumeration. As expected, the compiler needs to store the
strings used in the Months_Image
function when compiling this code.
Similarly, the compiler needs to store strings for the Months
enumeration for the Image
attribute.
Sometimes, we don't need to call the Image
attribute for a type. In
this case, we could save some storage by eliminating the strings associated
with the type. Here, we can use the Discard_Names
aspect to request the
compiler to reduce — as much as possible — the amount of storage
used for storing names for this type. Let's see an example:
procedure Show_Discard_Names is pragma Warnings (Off, "is not referenced"); type Months is (January, February, March, April, May, June, July, August, September, October, November, December) with Discard_Names; M : constant Months := January; begin null; end Show_Discard_Names;
In this example, the compiler attempts to not store strings associated with
the Months
type duration compilation.
Note that the Discard_Names
aspect is available for enumerations,
exceptions, and tagged types.
In the GNAT toolchain
If we add this statement to the Show_Discard_Names
procedure above:
Put_Line ("Month: "
& Months'Image (M));
we see that the application displays "0" instead of "JANUARY". This is
because GNAT doesn't store the strings associated with the Months
type when we use the Discard_Names
aspect for the Months
type. (Therefore, the Months'Image
attribute doesn't have that
information.) Instead, the compiler uses the integer value of the
enumeration, so that Months'Image
returns the corresponding string
for this integer value.
In the Ada Reference Manual