[Ada Information Clearinghouse]

Ada '83 Quality and Style:

Guidelines for Professional Programmers

Copyright 1989, 1991,1992 Software Productivity Consortium, Inc., Herndon, Virginia.

CHAPTER 8: Reusability

8.2 Robustness

The following guidelines improve the robustness of Ada code. It is easy to write code that depends on an assumption which you do not realize that you are making. When such a part is reused in a different environment, it can break unexpectedly. The guidelines below show some ways in which Ada code can be made to automatically conform to its environment, and some ways in which it can be made to check for violations of assumptions. Finally, some guidelines are given to warn you about errors which Ada does not catch as soon as you might like.
In this section...
8.2.1 Named Numbers
8.2.2 Unconstrained Arrays
8.2.3 Assumptions
8.2.4 Subtypes in Generic Specifications
8.2.5 Overloading in Generic Units
8.2.6 Hidden Tasks
8.2.7 Exceptions
Summary of Guidelines from this section


8.2.1 Named Numbers

guideline

example

------------------------------------------------------------------------ 
procedure Disk_Driver is

   -- In this procedure, a number of important disk parameters are 
   -- linked. 
   Number_Of_Sectors  : constant :=     4; 
   Number_Of_Tracks   : constant :=   200; 
   Number_Of_Surfaces : constant :=    18; 
   Sector_Capacity    : constant := 4_096;
   
   Track_Capacity   : constant := Number_Of_Sectors  * Sector_Capacity; 
   Surface_Capacity : constant := Number_Of_Tracks   * Track_Capacity; 
   Disk_Capacity    : constant := Number_Of_Surfaces * Surface_Capacity;
   
   type Sector_Range  is range 1 .. Number_Of_Sectors; 
   type Track_Range   is range 1 .. Number_Of_Tracks; 
   type Surface_Range is range 1 .. Number_Of_Surfaces;
   
   type Track_Map   is array (Sector_Range)  of ...; 
   type Surface_Map is array (Track_Range)   of Track_Map; 
   type Disk_Map    is array (Surface_Range) of Surface_Map;
   
begin  -- Disk_Driver 
   ... 
end Disk_Driver; 
------------------------------------------------------------------------

rationale

To reuse software that uses named numbers and static expressions appropriately, just one or a small number of constants need to be reset and all declarations and associated code are changed automatically. Apart from easing reuse, this reduces the number of opportunities for error and documents the meanings of the types and constants without using error-prone comments.

Language Ref Manual references: 3.2 Objects and Named Numbers, 4.9 Static Expressions and Static Subtypes


8.2.2 Unconstrained Arrays

guideline

example

   ... 
   type Vector is array (Vector_Index range <>) of Element; 
   type Matrix is array 
         (Vector_Index range <>, Vector_Index range <>) of Element; 
   ...
   
   --------------------------------------------------------------------- 
   procedure Matrix_Operation (Data : in     Matrix) is
   
      Workspace   : Matrix (Data'Range(1), Data'Range(2)); 
      Temp_Vector : Vector (Data'First(1) .. 2 * Data'Last(1)); 
   ... 
   ---------------------------------------------------------------------

rationale

Unconstrained arrays can be declared with their sizes dependent on formal parameter sizes. When used as local variables, their sizes change automatically with the supplied actual parameters. This facility can be used to assist in the adaption of a part since necessary size changes in local variables are taken care of automatically.

Language Ref Manual references: 3.6 Array Types, 12.1.2 Generic Formal Types, 12.3.4 Matching Rules for Formal Array Types


8.2.3 Assumptions

guideline

example

The following poorly written function documents, but does not check, its assumption:
   -- Assumption:  BCD value is less than 4 digits. 
   function Binary_To_BCD (Binary_Value : in     Natural) 
         return BCD;

The next example enforces conformance with its assumption, making the checking automatic, and the comment unnecessary:
   type Binary_Values is new Natural range 0 .. 9_999;
   
   function Binary_To_BCD (Binary_Value : in     Binary_Values) 
         return BCD;

The next example explicitly checks and documents its assumption:
   --------------------------------------------------------------------- 
   -- Out_Of_Range raised when BCD value exceeds 4  digits. 
   function Binary_To_BCD (Binary_Value : in     Natural) 
         return BCD is
         
      Maximum_Representable : constant Natural := 9_999;
      
   begin  -- Binary_To_BCD 
      if Binary_Value > Maximum_Representable then 
         raise Out_Of_Range; 
      end if;
      
      ... 
   end Binary_To_BCD; 
   ---------------------------------------------------------------------

rationale

Any part that is intended to be used again in another program, especially if the other program is likely to be written by other people, should be robust. It should defend itself against misuse by defining its interface to enforce as many assumptions as possible and by adding explicit defensive checks on anything which cannot be enforced by the interface.

note

You can restrict the ranges of values of the inputs by careful selection or construction of the types of the formal parameters. When you do so, the compiler-generated checking code may be more efficient than any checks you might write. Indeed, such checking is part of the intent of the strong typing in the language. This presents a challenge, however, for generic units where the user of your code selects the types of the parameters. Your code must be constructed so as to deal with any value of any type the user may choose to select for an instantiation.

Language Ref Manual references: 3.3 Types and Subtypes, 11.3 Raise Statements


8.2.4 Subtypes in Generic Specifications

guideline

example

In the following example, it appears that any value supplied for the generic formal object Object would be constrained to the range 1..10. It also appears that parameters passed at run-time to the Put routine in any instantiation, and values returned by the Get routine, would be similarly constrained.
   subtype Range_1_10 is Integer range 1 .. 10;

   --------------------------------------------------------------------- 
   generic
   
      Object : in out Range_1_10; 
      with procedure Put (Parameter : in     Range_1_10); 
      with function  Get  return             Range_1_10;
      
   package Input_Output is 
      ... 
   end Input_Output; 
   ---------------------------------------------------------------------

However, this is not the case. Given the following legal instantiation:
   subtype Range_15_30 is Integer range 15 .. 30; 
   Constrained_Object : Range_15_30 := 15;
   
   procedure Constrained_Put (Parameter : in     Range_15_30); 
   function  Constrained_Get  return             Range_15_30;
   
   package Constrained_Input_Output 
      is new Input_Output (Object => Constrained_Object, 
                           Put    => Constrained_Put, 
                           Get    => Constrained_Get);
                           
   ...

Object, Parameter, and the return value of Get are constrained to the range 15..30. Thus, for example, if the body of the generic package contains an assignment statement:

Object := 1;

Constraint_Error is raised when this instantiation is executed.

rationale

According to Sections 12.1.1(5) and 12.1.3(5) of the Ada Language Reference Manual (Department of Defense 1983), when constraint checking is performed for generic formal objects, and parameters and return values of generic formal subprograms, the constraints of the actual subtype (not the formal subtype or the base type) are enforced.

Thus, even with a generic unit which has been instantiated and tested many times, and with an instantiation which reported no errors at instantiation time, there can be a run-time error. Since the subtype constraints of the generic formal are ignored, the Ada Language Reference Manual (Department of Defense 1983) suggests using the name of a base type in such places to avoid confusion. Even so, you must be careful not to assume the freedom to use any value of the base type because the instantiation imposes the subtype constraints of the generic actual parameter. To be safe, always refer to specific values of the type via symbolic expressions containing attributes like 'First, 'Last, 'Pred, and 'Succ rather than via literal values.

The best solution is to introduce a new generic formal type parameter and use it in place of the subtype, as shown below:
------------------------------------------------------------------------ 
generic 
   type Object_Range is range <>; 
   Object : in out Object_Range;
   
   with procedure Put (Parameter : in     Object_Range); 
   with function  Get  return             Object_Range;
   
package Input_Output is 
   ... 
end Input_Output; 
------------------------------------------------------------------------

This is a clear statement by the developer of the generic unit that no assumptions are made about the Objects type other than that it is an integer type. This should reduce the likelihood of any invalid assumptions being made in the body of the generic unit.

For generics, attributes provide the means to maintain generality. It is possible to use literal values, but literals run the risk of violating some constraint. For example, assuming an array's index starts at one may cause a problem when the generic is instantiated for a zero-based array type.

Language Ref Manual references: 4.1.4 Attributes, 4.2 Literals, 12.1.1 Generic Formal Objects, 12.3 Generic Instantiation, A Predefined Language Attributes


8.2.5 Overloading in Generic Units

guideline

example

------------------------------------------------------------------------ 
generic 
   type Item is limited private;
   
package Input_Output is

   procedure Put (Value : in     Integer); 
   procedure Put (Value : in     Item);
   
end Input_Output; 
------------------------------------------------------------------------

rationale

If the generic package shown in the example above is instantiated with Integer (or any subtype of Integer) as the actual type corresponding to generic formal Value, then the two Put procedures have identical interfaces, and all calls to Put are ambiguous. Therefore, this package cannot be used with type Integer. In such a case, it is better to give unambiguous names to all subprograms. See Section 12.3(22) of the Ada Language Reference Manual (Department of Defense 1983) for more information.

Language Ref Manual references: 6.6 Parameter and Result Type Profile - Overloading of Subprograms, 8.7 The Context of Overload Resolution, 12.3 Generic Instantiation


8.2.6 Hidden Tasks

guideline

rationale

The effects of tasking becomes a major factor when reusable code enters the domain of real-time systems. Even though tasks may be used for other purposes, their effect on scheduling algorithms is still a concern and must be clearly documented. With the task clearly documented, the real-time programmer can then analyze performance, priorities, and so forth to meet timing requirements; or if necessary, he can modify or even redesign the component.

Concurrent access to data structures must be carefully planned to avoid errors, especially for data structures which are not atomic (see Chapter 6 for details). If a generic unit accesses one of its generic formal parameters (reads or writes the value of a generic formal object or calls a generic formal subprogram which reads or writes data) from within a task contained in the generic unit, then there is the possibility of concurrent access for which the user may not have planned. In such a case, the user should be warned by a comment in the generic specification.

Language Ref Manual references: 7.2 Package Specifications and Declarations, 9.3 Task Execution - Task Activation, 10.1.1 Context Clauses - With Clauses, 12.1.1 Generic Formal Objects


8.2.7 Exceptions

guideline

example

------------------------------------------------------------------------ 
generic

   type Number is limited private;
   
   with procedure Get (Value :    out Number);
   
procedure Process_Numbers;

------------------------------------------------------------------------ 
procedure Process_Numbers is

   Local : Number;
   
   procedure Perform_Cleanup_Necessary_For_Process_Numbers is 
         separate; 
   ...
   
begin  -- Process_Numbers 
   ...
   
   Catch_Exceptions_Generated_By_Get: 
      begin 
         Get(Local);
         
      exception 
         when others => 
            Perform_Cleanup_Necessary_For_Process_Numbers; 
            raise; 
      end Catch_Exceptions_Generated_By_Get;
      
   ... 
end Process_Numbers; 
------------------------------------------------------------------------

rationale

On most occasions, an exception is raised because an undesired event (such as floating-point overflow) has occurred. Such events often need to be dealt with entirely differently with different uses of a particular software part. It is very difficult to anticipate all the ways that users of the part may wish to have the exceptions handled. Passing the exception out of the part is the safest treatment.

In particular, when an exception is raised by a generic formal subprogram, the generic unit is in no position to understand why or to know what corrective action to take. Therefore, such exceptions should always be propagated back to the caller of the generic instantiation. However, the generic unit must first clean up after itself, restoring its internal data structures to a correct state so that future calls may be made to it after the caller has dealt with the current exception. For this reason, all calls to generic formal subprograms should be within the scope of a when others exception handler if the internal state is modified, as shown in the example above.

When a reusable part is invoked, the user of the part should be able to know exactly what operation (at the appropriate level of abstraction) has been performed. For this to be possible, a reusable part must always do all or none of its specified function; it must never do half. Therefore, any reusable part which terminates early by raising or propagating an exception should return to the caller with no effect on the internal or external state. The easiest way to do this is to test for all possible exceptional conditions before making any state changes (modifying internal state variables, making calls to other reusable parts to modify their states, updating files, etc.). When this is not possible, it is best to restore all internal and external states to the values which were current when the part was invoked before raising or propagating the exception. Even when this is not possible, it is important to document this potentially hazardous situation in the comment header of the specification of the part.

A similar problem arises with parameters of mode out or in out when exceptions are raised. The Ada language defines these modes in terms of "copy-in" and "copy-back" semantics, but leaves the actual parameter-passing mechanism undefined. When an exception is raised, the copy-back does not occur, but for an Ada compiler which passes parameters by reference, the actual parameter has already been updated. When parameters are passed by copy, the update does not occur. To reduce ambiguity, increase portability, and avoid situations where some but not all of the actual parameters are updated when an exception is raised, it is best to treat values of out and in out parameters like state variables, updating them only after it is certain that no exception will be raised.

Language Ref Manual references: 11.2 Exception Handlers, 11.3 Raise Statements, 11.4 Exception Handling, 12.2 Generic Bodies


Back to document index