Jump to content

Ada Programming/Types/access

From Wikibooks, open books for an open world

Ada. Time-tested, safe and secure.
Ada. Time-tested, safe and secure.

What's an Access Type?

[edit | edit source]

Access types in Ada are what other languages call pointers. They point to objects located at certain addresses. So normally one can think of access types as simple addresses (there are exceptions from this simplified view). Ada instead of saying points to talks of granting access to or designating an object.

Objects of access types are implicitly initialized with null, i.e. they point to nothing when not explicitly initialized.

Access types should be used rarely in Ada. In a lot of circumstances where pointers are used in other languages, there are other ways without pointers. If you need dynamic data structures, first check whether you can use the Ada Container library. Especially for indefinite record or array components, the Ada 2012 package Ada.Containers.Indefinite_Holders (RM A.18.18 [Annotated]) can be used instead of pointers.

There are four kinds of access types in Ada: Pool access types - General access types - Anonymous access types - Access to subprogram types.

Pool access

[edit | edit source]

A pool access type handles accesses to objects which were created on some specific heap (or storage pool as it is called in Ada). A pointer of these types cannot point to a stack or library level (static) object or an object in a different storage pool. Therefore, conversion between pool access types is illegal. (Unchecked_Conversion may be used, but note that deallocation via an access object with a storage pool different from the one it was allocated with is erroneous.)

type Person is record
  First_Name : String (1..30);
  Last_Name  : String (1..20);
end record;

type Person_Access is access Person;

A storage size clause may be used to limit the corresponding (implementation defined anonymous) storage pool. A storage size clause of 0 disables calls of an allocator.

for Person_Access'Storage_Size use 0;

The storage pool is implementation defined if not specified. Ada supports user defined storage pools, so you can define the storage pool with

for Person_Access'Storage_Pool use Pool_Name;

Objects in a storage pool are created with the keyword new:

Father: Person_Access := new Person;                                          -- uninitialized
Mother: Person_Access := new Person'(Mothers_First_Name, Mothers_Last_Name);  -- initialized

You access the object in the storage pool by appending .all. Mother.all is the complete record; components are denoted as usual with the dot notation: Mother.all.First_Name. When accessing components, implicit dereferencing (i.e. omitting all) can serve as a convenient shorthand:

Mother.all := (Last_Name => Father.Last_Name, First_Name => Mother.First_Name);  -- marriage

Implicit dereferencing also applies to arrays:

  type Vector is array (1 .. 3) of Complex;
  type Vector_Access is access Vector;

  VA: Vector_Access := new Vector;
  VB: array (1 .. 3) of Vector_Access := (others => new Vector);

  C1: Complex := VA (3);    -- a shorter equivalent for VA   .all (3)
  C2: Complex := VB (3)(1); -- a shorter equivalent for VB(3).all (1)

Be careful to discriminate between deep and shallow copies when copying with access objects:

Obj1.all := Obj2.all;  -- Deep copy: Obj1 still refers to an object different from Obj2, but it has the same content
Obj1 := Obj2;          -- Shallow copy: Obj1 now refers to the same object as Obj2

Deleting objects from a storage pool

[edit | edit source]

Although the Ada standard mentions a garbage collector, which would automatically remove all unneeded objects that have been created on the heap (when no storage pool has been defined), only Ada compilers targeting a virtual machine like Java or .NET actually have garbage collectors.

When an access type goes out of scope, the corresponding still allocated data items are finalized (i.e. they do no longer exist) in an arbitrary order; the allocated memory, however, is only freed when the attribute Storage_Size has been defined for the access type via an attribute_definition clause. (Note: Finalization and deallocation are different things!)

Proof

[edit | edit source]

The following are excerpts from the Ada Reference Manual. The ellipses stand for parts not relevant for the case.

RM 3.10(7/1) There are ... access-to-object types, whose values designate objects... Associated with an access-to-object type is a storage pool; several access types may share the same storage pool. ... A storage pool is an area of storage used to hold dynamically allocated objects (called pool elements) created by allocators.

(8) Access-to-object types are further subdivided into pool-specific access types, whose values can designate only the elements of their associated storage pool...

RM 7.6(1) ... Every object is finalized before being destroyed (for example, by leaving a subprogram_body containing an object_declaration, or by a call to an instance of Unchecked_Deallocation)...

RM 7.6.1(5) For the finalization of an object:

(6/3) If the full type of the object is an elementary type, finalization has no effect;

(7/3) If the full type of the object is a tagged type, and the tag of the object identifies a controlled type, the Finalize procedure of that controlled type is called;

(10) Immediately before an instance of Unchecked_Deallocation reclaims the storage of an object, the object is finalized. If an instance of Unchecked_Deallocation is never applied to an object created by an allocator, the object will still exist when the corresponding master completes, and it will be finalized then.

(11.1/3) Each nonderived access type T has an associated collection, which is the set of objects created by allocators of T, or of types derived from T. Unchecked_Deallocation removes an object from its collection. Finalization of a collection consists of finalization of each object in the collection, in an arbitrary order…

RM 13.11(1) Each access-to-object type has an associated storage pool. The storage allocated by an allocator comes from the pool; instances of Unchecked_Deallocation return storage to the pool. Several access types can share the same pool.

(2/2) A storage pool is a variable of a type in the class rooted at Root_Storage_Pool, which is an abstract limited controlled type. By default, the implementation chooses a standard storage pool for each access-to-object type…

(11) A storage pool type (or pool type) is a descendant of Root_Storage_Pool. The elements of a storage pool are the objects allocated in the pool by allocators.

(15) Storage_Size or Storage_Pool may be specified for a nonderived access-to-object type via an attribute_definition_clause...

(17) If Storage_Pool is not specified for a type defined by an access_to_object_definition, then the implementation chooses a standard storage pool for it in an implementation-defined manner...

(18/4) If Storage_Size is specified for an access type T, an implementation-defined pool P is used for the type. The Storage_Size of P is at least that requested, and the storage for P is reclaimed when the master containing the declaration of the access type is left...

Example

[edit | edit source]

The following program will compile but will fail the accessibility check on runtime with an exception.

with Ada.Text_IO;
use Ada.Text_IO;

procedure Main is
   function Accessibility_Check_Fail
     return access String
   is
      -- Declare a new access type locally.
      -- All memory with this type will be finalized but not freed
      -- when the this type goes out of scope.
      type A_Type is access String;  -- no Storage_Size defined
      
      X : A_Type := new String'("x");  -- storage will be lost
      Y : access String;  -- defined locally
   begin
      Y := X;  -- data defined in a local pool will be finalized when function returns
      return Y;  -- exception should be raised
   end Accessibility_Check_Fail;
begin
   -- Accessibility check will fail because the accessiblity level associated
   -- with Y is deeper than the accessibility level of this scope.
   Put_Line(Accessibility_Check_Fail.all);
end Main;

There is also a pragma Controlled, which, when applied to such an access type, prevents automatic garbage collection of objects created with it. Note that pragma Controlled was dropped from Ada 2012, subpools for storage management replacing it. See RM 2012 13.11.3 [Annotated] and 13.11.4 [Annotated].

Therefore, in order to delete an object from the heap, you need the generic unit Ada.Unchecked_Deallocation. Apply utmost care to not create dangling pointers when deallocating objects as is shown in the example below. (And note that deallocating objects with a different access type than the one with which they were created is erroneous when the corresponding storage pools are different.)

with Ada.Unchecked_Deallocation;

procedure Deallocation_Sample is

   type Vector     is array (Integer range <>) of Float;
   type Vector_Ref is access Vector;

   procedure Free_Vector is new Ada.Unchecked_Deallocation
      (Object => Vector, Name => Vector_Ref);
  
   VA, VB: Vector_Ref;
   V     : Vector;

begin

   VA     := new Vector (1 .. 10);
   VB     := VA;  -- points to the same location as VA

   VA.all := (others => 0.0);

   --  ... Do whatever you need to do with the vector

   Free_Vector (VA); -- The memory is deallocated and VA is now null

   V := VB.all;  -- VB is not null, access to a dangling pointer is erroneous

end Deallocation_Sample;

It is exactly because of this problem with dangling pointers that the deallocation operation is called unchecked. It is the chore of the programmer to take care that this does not happen.

Since Ada allows for user-defined storage pools, you could also try a garbage collector library.

Constructing Reference Counting Pointers

[edit | edit source]

You can find some implementations of reference counting pointers, called Safe or Smart Pointers, on the net. Using such a type prevents caring about deallocation, since this will automatically be done when there are no more pointers to an object. But be careful - most of those implementations do not prevent deliberate deallocation, thus undermining the alleged safety attained with their use.

A nice tutorial how to construct such a type can be found in a series of Gems on the AdaCore web site.

Gem #97: Reference Counting in Ada – Part 1 This little gem constructs a simple reference counted pointer that does not prevent deallocation, i.e. is inherently unsafe.

Gem #107: Preventing Deallocation for Reference-counted Types This further gem describes how to arrive at a pointer type whose safety cannot be compromised (tasking issues aside). The cost of this improved safety is awkward syntax.

Gem #123: Implicit Dereferencing in Ada 2012 This gem shows how to simplify the syntax with the new Ada 2012 generation. (Admittedly, this gem is a bit unrelated to reference counting since the new language feature can be applied to any kind of container.)

General access

[edit | edit source]

General access types grant access to objects created on any storage pool, on the stack or at library level (static). They come in two versions, granting either read-write access or read-only access. Conversions between general access types are allowed, but subject to certain access level checks.

Dereferencing is like for pool access types. Objects (other than pool objects) to be referenced have to be declared aliased, and references to them are created with the attribute 'Access. Access level restrictions prevent accesses to objects from outliving the accessed object, which would make the program erroneous. The attribute 'Unchecked_Access omits the corresponding checks.

Access to Variable

[edit | edit source]

When the keyword all is used in their definition, they grant read-write access.

type Day_Of_Month is range 1 .. 31;            
type Day_Of_Month_Access is access all Day_Of_Month;

Access to Constant

[edit | edit source]

General access types granting read-only access to the referenced object use the keyword constant in their definition. The referenced object may be a constant or a variable.

type Day_Of_Month is range 1 .. 31;            
type Day_Of_Month_Access is access constant Day_Of_Month;

Some examples

[edit | edit source]
 type General_Pointer  is access all      Integer;
 type Constant_Pointer is access constant Integer;

 I1: aliased constant Integer := 10;
 I2: aliased Integer;

 P1: General_Pointer  := I1'Access;  -- illegal
 P2: Constant_Pointer := I1'Access;  -- OK, read only
 P3: General_Pointer  := I2'Access;  -- OK, read and write
 P4: Constant_Pointer := I2'Access;  -- OK, read only

 P5: constant General_Pointer := I2'Access;  -- read and write only to I2

Anonymous access

[edit | edit source]

Also Anonymous access types come in two versions like general access types, granting either read-write access or read-only access depending on whether the keyword constant appears.

An anonymous access can be used as a parameter to a subprogram or as a discriminant. Here are some examples:

procedure Modify (Some_Day: access          Day_Of_Month);
procedure Test   (Some_Day: access constant Day_Of_Month);  -- Ada 2005 only
task type Thread (Execute_For_Day: access Day_Of_Month) is
   ...
end Thread;
type Day_Data (Store_For_Day: access Day_Of_Month) is record
  -- components
end record;

Before using an anonymous access, you should consider a named access type or, even better, consider if the "out" or "in out" modifier is not more appropriate.

This language feature is only available from Ada 2005 on.

In Ada 2005, anonymous accesses are allowed in more circumstances:

type Object is record
  M   : Integer;
  Next: access Object;
end record;

X: access Integer;

function F return access constant Float;

Implicit Dereference

[edit | edit source]

This language feature has been introduced in Ada 2012.

Ada 2012 simplifies accesses to objects via pointers with new syntax.

Imagine you have a container holding some kind of elements.

type Container   is private;
type Element_Ptr is access Element;

procedure Put (X: Element; Into: in out Container);

Now, how do you access elements stored in the container. Of course, you can retrieve them by

function Get (From: Container) return Element;

This will however copy the element, which is unfortunate if the element is big. You get direct access with

function Get (From: Container) return Element_Ptr;

Now, pointers are dangerous since you might easily create dangling pointers like so:

P: Element_Ptr := Get (Cont);
P.all := E;
Free (P);
... Get (Cont) -- this is now a dangling pointer

Use of an accessor object instead of an access type can prevent inadvertent deallocation (this is still Ada 2005):

type Accessor (Data: not null access Element) is limited private;  -- read/write access
function Get (From: Container) return Accessor;

(For the null exclusion not null in the declaration of the discriminant, see below). Access via such an accessor is safe: The discriminant can only be used for dereferencing, it cannot be copied to an object of type Element_Ptr because its accessibility level is deeper. In the form above, the accessor provides read and write access. If the keyword constant is added, only read access is possible.

type Accessor (Data: not null access constant Element) is limited private;  -- only read access

Access to the container object now looks like so:

Get (Cont).all      := E;  -- via access type: dangerous
Get (Cont).Data.all := E;  -- via accessor: safe, but ugly

Here the new Ada 2012 feature of aspects comes along handy; for the case at hand, the aspect Implicit_Dereference is the one we need:

type Accessor (Data: not null access Element) is limited private
   with Implicit_Dereference => Data;

Now rather than writing the long and ugly function call of above, we can just omit the discriminant and its dereference like so:

Get (Cont).Data.all := E;  -- Ada 2005 via accessor: safe, but ugly
Get (Cont)          := E;  -- Ada 2012 implicit dereference

Note that the call Get (Cont) is overloaded — it can denote the accessor object or the element, the compiler will select the correct interpretation depending on context.

Null exclusions

[edit | edit source]

This language feature is only available from Ada 2005 on.

All access subtypes can be modified with not null, objects of such a subtype can then never have the value null, so initializations are compulsory.

type    Day_Of_Month_Access          is access   Day_Of_Month;
subtype Day_Of_Month_Not_Null_Access is not null Day_Of_Month_Access;

The language also allows to declare the first subtype directly with a null exclusion:

type Day_Of_Month_Access is not null access Day_Of_Month;

However, in nearly all cases this is not a good idea because it renders objects of this type nearly unusable (for example, you are unable to free the allocated memory). Not null accesses are intended for access subtypes, object declarations, and subprogram parameters.[1]

Access to Subprogram

[edit | edit source]

An access to subprogram allows the caller to call a subprogram without knowing its name nor its declaration location. One of the uses of this kind of access is the well known callbacks.

type Callback_Procedure is access procedure (Id  : Integer;
                                             Text: String);

type Callback_Function is access function (The_Alarm: Alarm) return Natural;

For getting an access to a subprogram, the attribute Access is applied to a subprogram name with the proper parameter and result profile.

procedure Process_Event (Id  : Integer;
                         Text: String);

My_Callback: Callback_Procedure := Process_Event'Access;

Anonymous access to Subprogram

[edit | edit source]

This language feature is only available from Ada 2005 on.

procedure Test (Call_Back: access procedure (Id: Integer; Text: String));

There is now no limit on the number of keyword in a sequence:

function F return access function return access function return access Some_Type;

This is a function that returns the access to a function that in turn returns an access to a function returning an access to some type.

Access FAQ

[edit | edit source]

A few "Frequently Asked Question" and "Frequently Encountered Problems" (mostly from C users) regarding Ada's access types.

Access vs. access all

[edit | edit source]

An access all can do anything a simple access can do. So one might ask: "Why use simple access at all?" - And indeed some programmers never use simple access.

Unchecked_Deallocation is always dangerous if misused. It is just as easy to deallocate a pool-specific object twice, and just as dangerous as deallocating a stack object. The advantage of "access all" is that you may not need to use Unchecked_Deallocation at all.

Moral: if you have (or may have) a valid reason to store an 'Access or 'Unchecked_Access into an access object, then use "access all" and don't worry about it. If not, the mantra of "least privilege" suggests that the "all" should be left out (don't enable capabilities that you are not going to use).

The following (perhaps disastrous) example will try to deallocate a stack object:

declare

  type Day_Of_Month is range 1 .. 31;            
  type Day_Of_Month_Access is access all Day_Of_Month;

  procedure Free is new Ada.Unchecked_Deallocation
      (Object => Day_Of_Month,
       Name   => Day_Of_Month_Access);

  A  : aliased Day_Of_Month;
  Ptr: Day_Of_Month_Access := A'Access;

begin

   Free(Ptr);

end;

With a simple access you know at least that you won't try to deallocate a stack object. The reason is that access does not allow pointers to be created from stack objects.

Access vs. System.Address

[edit | edit source]

An access can be something different from a mere memory address, it may be something more. For example, an "access to String" often needs some way of storing the string size as well. If you need a simple address and are not concerned about strong typing, use the System.Address type.

C compatible pointer

[edit | edit source]

The correct way to create a C compatible access is to use pragma Convention:

type Day_Of_Month is range 1 .. 31;
for  Day_Of_Month'Size use Interfaces.C.int'Size;

pragma Convention (Convention => C,
                   Entity     => Day_Of_Month);

type Day_Of_Month_Access is access Day_Of_Month;

pragma Convention (Convention => C,
                   Entity     => Day_Of_Month_Access);

pragma Convention should be used on any type you want to use in C. The compiler will warn you if the type cannot be made C compatible.

You may also consider the following - shorter - alternative when declaring Day_Of_Month:

type Day_Of_Month is new Interfaces.C.int range 1 .. 31;

Before you use access types in C, you should consider using the normal "in", "out" and "in out" modifiers. pragma Export and pragma Import know how parameters are usually passed in C and will use a pointer to pass a parameter automatically where C would have used them as well. Of course the RM contains precise rules on when to use a pointer for "in", "out", and "in out" - see "B.3: Interfacing with C [Annotated]".

Where is void*?

[edit | edit source]

While actually a problem for "interfacing with C", here are some possible solutions:

procedure Test is

  subtype Pvoid is System.Address;

  -- the declaration in C looks like this:
  -- int C_fun(int *)
  function C_fun (pv: Pvoid) return Integer;
  pragma Import (Convention    => C,
                 Entity        => C_fun,     -- any Ada name
                 External_Name => "C_fun");  -- the C name

  Pointer: Pvoid;

  Input_Parameter: aliased Integer := 32;
  Return_Value   : Integer;

begin

  Pointer      := Input_Parameter'Address;
  Return_Value := C_fun (Pointer);

end Test;

Less portable, but perhaps more usable (for 32 bit CPUs):

type void is mod 2 ** 32;
for void'Size use 32;

With GNAT you can get 32/64 bit portability by using:

type void is mod System.Memory_Size;
for void'Size use System.Word_Size;

Closer to the true nature of void - pointing to an element of zero size is a pointer to a null record. This also has the advantage of having a representation for void and void*:

type Void is null record;
pragma Convention (C, Void);

type Void_Ptr is access all Void;
pragma Convention (C, Void_Ptr);

Thin and Fat Access Types

[edit | edit source]

The difference between an access type and an address will be detailed in the following. The term pointer is used because this is usual terminology.

There is a predefined unit System.Address_to_Access_Conversion converting back and forth between access values and addresses. Use these conversions with care, as is explained below.

Thin Pointers

[edit | edit source]

Thin pointers grant access to constrained subtypes.

type Int     is range -100 .. +500;
type Acc_Int is access Int;

type Arr     is array (1 .. 80) of Character;
type Acc_Arr is access Arr;

Objects of subtypes like these have a static size, so a simple address suffices to access them. In the case of arrays, this is generally the address of the first element.

For pointers of this kind, use of System.Address_to_Access_Conversion is safe.

Fat Pointers

[edit | edit source]
type Unc     is array (Integer range <>) of Character;
type Acc_Unc is access Unc;

Objects of subtype Unc need a constraint, i.e. a start and a stop index, thus pointers to them need also to include those. So a simple address like the one of the first component is not sufficient. Note that A'Address is the same as A(A'First)'Address for any array object.

For pointers of this kind, System.Address_to_Access_Conversion will probably not work satisfactorily.

Example

[edit | edit source]
CO: aliased Unc (-1 .. +1) := (-1 .. +1 => ' ');
UO: aliased Unc            := (-1 .. +1 => ' ');

Here, CO is a nominally constrained object, a pointer to it need not store the constraint, i.e. a thin pointer suffices. In contrast, UO is an object of a nominally unconstrained subtype, its actual subtype is constrained by the initial value.

A: Acc_Unc            := CO'Access;  -- illegal
B: Acc_Unc            := UO'Access;  -- OK
C: Acc_Unc (CO'Range) := CO'Access;  -- also illegal

The relevant paragraphs in the RM are difficult to understand. In short words:

An access type's target type is called the designated subtype, in our example Unc. RM 3.10.2 [Annotated](27.1/2) requires that Acc_Unc's designated subtype statically match the nominal subtype of the object.

Now the nominal subtype of CO is the constrained anonymous subtype Unc (-1 .. +1), the nominal subtype of UO is the unconstrained subtype Unc. In the illegal cases, the designated and nominal subtypes do not statically match.

See also

[edit | edit source]

Wikibook

[edit | edit source]

Ada Reference Manual

[edit | edit source]

Ada 95

[edit | edit source]

Ada 2005

[edit | edit source]

Newest RM

[edit | edit source]

Ada Quality and Style Guide

[edit | edit source]

References

[edit | edit source]