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¶
The following sections provide 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 aspect¶
Before we discuss the size attributes, however, we briefly look into the
Size aspect.
When we define a type, the compiler selects the most appropriate size for that data type. For example:
package Custom_Types is
type My_Int is range 0 .. 127;
end Custom_Types;
with Ada.Text_IO; use Ada.Text_IO;
with Custom_Types; use Custom_Types;
procedure Show_Sizes is
begin
Put_Line ("Size of My_Int: "
& My_Int'Size'Image
& " bits");
end Show_Sizes;
On a typical desktop PC, the compiler selects a size of 7 bits for the custom
integer type My_Int. (In this example, we use the
Size attribute, which we discuss soon.)
This means that, in order to represent objects of My_Int type, the
compiler has to reserve at least 7 bits. (In other words, this is the
minimum requirement for that data type. We revisit this topic later on in this
section.)
Depending on the requirements of your target system, however, you might have to
request the compiler to select a specific size for your data type. You can do
this by using the Size aspect. For example:
package Custom_Types is
type My_Int is range 0 .. 127
with Size => 24;
end Custom_Types;
with Ada.Text_IO; use Ada.Text_IO;
with Custom_Types; use Custom_Types;
procedure Show_Sizes is
begin
Put_Line ("Size of My_Int: "
& My_Int'Size'Image
& " bits");
end Show_Sizes;
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
My_Intshould be stored using at least 24 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
My_Intshould be stored using at least 24 bits. Do you agree?"(Ada compiler) "For the target platform that you selected, I cannot possibly do it! COMPILATION ERROR!"
Size attributes¶
The Size attribute is a function that returns the minimum number of bits
necessary to represent objects of that type. Remember that this number is
selected by the compiler — unless, of course, we use the Size
aspect as a request to the compiler to verify that the expected size can be
used on the target platform.
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_S24 is range 0 .. 127
with Size => 24;
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_S24;
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_S24'Size: "
& UInt_7_S24'Size'Image);
Put_Line ("UInt_7_S24'Object_Size: "
& UInt_7_S24'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_S24'Size: 24
UInt_7_S24'Object_Size: 32
V2'Size: 32
As we said above, 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.
Similarly, for the UInt_7_S24 type, the Size attribute tells us
that the type requires a minimum number of 24 bits to represent objects of that
type, while the actual storage makes use of 32 bits. Note that, in this case,
we've used the Size aspect (with Size => 24) to request 24 bits
for the size of the UInt_7_S24 type.
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 (17,179,869,176 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_Storageattribute 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_Storageattribute 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) = Trueimplies 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_StorageandOverlaps_Storageattributes.Process, which are supposed to process an input arrayXand 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 theAarray as input and output arguments. In this case, a single component of theAarray 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 arrayAfor 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:
![digraph foo {
"Record_R" [
label = "{ position | component } | { { 0 | 1 | 2 | 3 } | A } | { { 4 | 5 | 6 | 7 } | B }"
shape = "record"
];
}](../../../../_images/graphviz-d62f6f66aee1711afb35bad472a1712e2574933b.png)
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:
![digraph foo {
"Record_R" [
label = "{ position | bits | component } | { { { 0 | #0 .. 7 } | { 1 | #8 .. #15 } | { 2 | #16 .. #23 } | { 3 | #24 .. #31 } } | A } | { { { 4 | #0 .. 7 } | { 5 | #8 .. #15 } | { 6 | #16 .. #23 } | { 7 | #24 .. #31 } } | B }"
shape = "record"
];
}](../../../../_images/graphviz-e3754734e980285106c3dd5ddba8e567eec55b9d.png)
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:
![digraph foo {
"Record_R" [
label = "{ position | bits | component } | { { { 0 | #0 .. 7 } | { 1 | #8 .. #15 } | { 2 | #16 .. #23 } | { 3 | #24 .. #31 } } | A } | { 4 | | } | { { { 5 | #0 .. 7 } | { 6 | #8 .. #15 } | { 7 | #16 .. #23 } | { 8 | #24 .. #31 } } | B }"
shape = "record"
];
}](../../../../_images/graphviz-a3280d124a2c96c98eafecf7f427bfaa8dfd49a5.png)
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 72 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:
![digraph foo {
"Record_R" [
label = "{ bit | component } | { { 0 | 1 } | S } | { { 2 | 3 } | (reserved) } | { 4 | Error } | { { 5 | 6 | 7 } | V1 }"
shape = "record"
];
}](../../../../_images/graphviz-d8f99e16d47070332c9c9e72c425929a629a70da.png)
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.
(We talk again about variant records in
another chapter.)
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:
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.)
Note that we're using the Atomic aspect here, which we discuss
in another chapter.
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