Skip to main content

8.2 Robustness

The following guidelines improve the robustness of Ada code. It is easy to write code that depends on an assumption that you do not realize that you are making. When such a part is reused in a different environment, it can break unexpectedly. The guidelines in this section 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 that Ada does not catch as soon as you might like.

Named Numbers

guideline

  • Use named numbers and static expressions to allow multiple dependencies to be linked to a small number of symbols.

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.

Unconstrained Arrays

guideline

  • Use unconstrained array types for array formal parameters and array return values.
  • Make the size of local variables depend on actual parameter size, where appropriate.

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 adaptation of a part because necessary size changes in local variables are taken care of automatically.

Minimizing and Documenting Assumptions

guideline

  • Minimize the number of assumptions made by a unit.
  • For assumptions that cannot be avoided, use subtypes or constraints to automatically enforce conformance.
  • For assumptions that cannot be automatically enforced by subtypes, add explicit checks to the code.
  • Document all assumptions.
  • If the code depends upon the implementation of a specific Special Needs Annex for proper operation, document this assumption in the code.

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:

   subtype Binary_Values is 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 that cannot be enforced by the interface. By documenting dependencies on a Special Needs Annex, you warn the user that he should only reuse the component in a compilation environment that provides the necessary support.

notes

You can restrict the ranges of values of the inputs by careful selection or construction of the subtypes 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 to deal with any value of any subtype the user may choose to select for an instantiation.

Subtypes in Generic Specifications

guideline

  • Use first subtypes when declaring generic formal objects of mode in out.
  • Beware of using subtypes as subtype marks when declaring parameters or return values of generic formal subprograms.
  • Use attributes rather than literal values.

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

The language specifies that 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) are enforced (Ada Reference Manual 1995, §§12.4"> and 12.6).Thus, the subtype specified in a formal in out object parameter and the subtypes specified in the profile of a formal subprogram need not match those of the actual object or subprogram.

Thus, even with a generic unit that has been instantiated and tested many times and with an instantiation that reported no errors at instantiation time, there can be a run-time error. Because the subtype constraints of the generic formal are ignored, the Ada Reference Manual (1995, §§12.4 and 12.6) 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.

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 that an array's index starts at 1 may cause a problem when the generic is instantiated for a zero-based array type.

notes

Adding a generic formal parameter that defines the subtype of the generic formal object does not address the ramifications of the constraint checking rule discussed in the above rationale. You can instantiate the generic formal type with any allowable subtype, and you are not guaranteed that this subtype is the first subtype:

generic
type Object_Range is range <>;
Objects : in out Object_Range;
...
package X is
...
end X;

You can instantiate the subtype Object_Range with any Integer subtype, for example, Positive. However, the actual variable Object can be of Positive'Base, i.e., Integer and its value are not guaranteed to be greater than 0.

Overloading in Generic Units

guideline

  • Be careful about overloading the names of subprograms exported by the same generic package.

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 Item, 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 the Ada Reference Manual (1995, §12.3) for more information.

Hidden Tasks

guideline

  • Within a specification , document any tasks that would be activated by with'ing the specification and by using any part of the specification.
  • Document which generic formal parameters are accessed from a task hidden inside the generic unit.
  • Document any multithreaded components.

rationale

The effects of tasking become 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 datastructures must be carefully planned to avoid errors, especially for data structures that 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 that 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.

Exceptions

guideline

  • Propagate exceptions out of reusable parts. Handle exceptions within reusable parts only when you are certain that the handling is appropriate in all circumstances.
  • Propagate exceptions raised by generic formal subprograms after performing any cleanup necessary to the correct operation of future invocations of the generic instantiation.
  • Leave state variables in a valid state when raising an exception.
  • Leave parameters unmodified when raising an exception.

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 that 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 that 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 distinguishes between "by-copy" and "by-reference" parameter passing. In some cases, "by-copy" is required; in other cases, "by-reference" is required; and in the remaining cases, either mechanism is allowed. The potential problem arises in those cases where the language does not specify the parameter passing mechanism to use. When an exception is raised, the copy-back does not occur, but for an Ada compiler, which passes parameters by reference (in those cases where a choice is allowed), 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. See also Guideline 7.1.8.

notes

A reusable part could range from a low-level building block (e.g., data structure, sorting algorithm, math function) to a large reusable subsystem. The lower level the building block, the less likely that the reusable part will know how to handle exceptions or produce meaningful results. Thus, the low-level parts should propagate exceptions. A large reusable subsystem, however, should be able to handle any anticipated exceptions independently of the variations across which it is reused.