Ada 95 Quality and Style Guide                      Chapter 5

Back to Sections 5.0 through 5.6.3

5.6.4 Loops

guideline

example

To iterate over all elements of an array:

for I in Array_Name'Range loop

   ...

end loop;

To iterate over all elements in a linked list:


Pointer := Head_Of_List;

while Pointer /= null loop

   ...

   Pointer := Pointer.Next;

end loop;

Situations requiring a "loop and a half" arise often. For this, use:


P_And_Q_Processing:

   loop

      P;

      exit P_And_Q_Processing when Condition_Dependent_On_P;

      Q;

   end loop P_And_Q_Processing;

rather than:


P;

while not Condition_Dependent_On_P loop

   Q;

   P;

end loop;

rationale

A for loop is bounded, so it cannot be an "infinite loop." This is enforced by the Ada language, which requires a finite range in the loop specification and does not allow the loop counter of a for loop to be modified by a statement executed within the loop. This yields a certainty of understanding for the reader and the writer not associated with other forms of loops. A for loop is also easier to maintain because the iteration range can be expressed using attributes of the data structures upon which the loop operates, as shown in the example above where the range changes automatically whenever the declaration of the array is modified. For these reasons, it is best to use the for loop whenever possible, that is, whenever simple expressions can be used to describe the first and last values of the loop counter.

The while loop has become a very familiar construct to most programmers. At a glance, it indicates the condition under which the loop continues. Use the while loop whenever it is not possible to use the for loop but when there is a simple Boolean expression describing the conditions under which the loop should continue, as shown in the example above.

The plain loop statement should be used in more complex situations, even if it is possible to contrive a solution using a for or while loop in conjunction with extra flag variables or exit statements. The criteria in selecting a loop construct are to be as clear and maintainable as possible. It is a bad idea to use an exit statement from within a for or while loop because it is misleading to the reader after having apparently described the complete set of loop conditions at the top of the loop. A reader who encounters a plain loop statement expects to see exit statements.

There are some familiar looping situations that are best achieved with the plain loop statement. For example, the semantics of the Pascal repeat until loop, where the loop is always executed at least once before the termination test occurs, are best achieved by a plain loop with a single exit at the end of the loop. Another common situation is the "loop and a half" construct, shown in the example above, where a loop must terminate somewhere within the sequence of statements of the body. Complicated "loop and a half" constructs simulated with while loops often require the introduction of flag variables or duplication of code before and during the loop, as shown in the example. Such contortions make the code more complex and less reliable.

Minimize the number of ways to exit a loop to make the loop more understandable to the reader. It should be rare that you need more than two exit paths from a loop. When you do, be sure to use exit statements for all of them, rather than adding an exit statement to a for or while loop.

5.6.5 Exit Statements

guideline

example

See the examples in Guidelines 5.1.1 and 5.6.4.

rationale

It is more readable to use exit statements than to try to add Boolean flags to a while loop condition to simulate exits from the middle of a loop. Even if all exit statements would be clustered at the top of the loop body, the separation of a complex condition into multiple exit statements can simplify and make it more readable and clear. The sequential execution of two exit statements is often more clear than the short-circuit control forms.

The exit when form is preferable to the if ... then exit form because it makes the word exit more visible by not nesting it inside of any control construct. The if ... then exit form is needed only in the case where other statements, in addition to the exit statement, must be executed conditionally. For example:


Process_Requests:

   loop

      if Status = Done then



         Shut_Down;

         exit Process_Requests;



      end if;



      ...



   end loop Process_Requests;

Loops with many scattered exit statements can indicate fuzzy thinking regarding the loop's purpose in the algorithm. Such an algorithm might be coded better some other way, for example, with a series of loops. Some rework can often reduce the number of exit statements and make the code clearer.

See also Guidelines 5.1.3 and 5.6.4.

5.6.6 Recursion and Iteration Bounds

guideline

example

Establishing an iteration bound:

Safety_Counter := 0;

Process_List:

   loop

      exit when Current_Item = null;

      ...

      Current_Item := Current_Item.Next;

      ...

      Safety_Counter := Safety_Counter + 1;

      if Safety_Counter > 1_000_000 then

         raise Safety_Error;

      end if;

   end loop Process_List;

Establishing a recursion bound:


subtype Recursion_Bound is Natural range 0 .. 1_000;



procedure Depth_First (Root           : in     Tree;

                       Safety_Counter : in     Recursion_Bound

                                      := Recursion_Bound'Last) is

begin

   if Root /= null then

      if Safety_Counter = 0 then

         raise Recursion_Error;

      end if;

      Depth_First (Root           => Root.Left_Branch,

                   Safety_Counter => Safety_Counter - 1);



      Depth_First (Root           => Root.Right_Branch,

                   Safety_Counter => Safety_Counter - 1);

      ... -- normal subprogram body

   end if;

end Depth_First;

Following are examples of this subprogram's usage. One call specifies a maximum recursion depth of 50. The second takes the default (1,000). The third uses a computed bound:


Depth_First(Root => Tree_1, Safety_Counter => 50);

Depth_First(Tree_2);

Depth_First(Root => Tree_3, Safety_Counter => Current_Tree_Height);

rationale

Recursion, and iteration using structures other than for statements, can be infinite because the expected terminating condition does not arise. Such faults are sometimes quite subtle, may occur rarely, and may be difficult to detect because an external manifestation might be absent or substantially delayed.

By including counters and checks on the counter values, in addition to the loops themselves, you can prevent many forms of infinite loops. The inclusion of such checks is one aspect of the technique called Safe Programming (Anderson and Witty 1978).

The bounds of these checks do not have to be exact, just realistic. Such counters and checks are not part of the primary control structure of the program but a benign addition functioning as an execution-time "safety net," allowing error detection and possibly recovery from potential infinite loops or infinite recursion.

notes

If a loop uses the for iteration scheme (Guideline 5.6.4), it follows this guideline.

exceptions

Embedded control applications have loops that are intended to be infinite. Only a few loops within such applications should qualify as exceptions to this guideline. The exceptions should be deliberate (and documented) policy decisions.

This guideline is most important to safety critical systems. For other systems, it may be overkill.

5.6.7 Goto Statements

guideline

rationale

A goto statement is an unstructured change in the control flow. Worse, the label does not require an indicator of where the corresponding goto statement(s) are. This makes code unreadable and makes its correct execution suspect.

Other languages use goto statements to implement loop exits and exception handling. Ada's support of these constructs makes the goto statement extremely rare.

notes

If you should ever use a goto statement, highlight both it and the label with blank space. Indicate at the label where the corresponding goto statement(s) may be found.

5.6.8 Return Statements

guideline

example

The following code fragment is longer and more complex than necessary:

if Pointer /= null then

   if Pointer.Count > 0 then

      return True;

   else  -- Pointer.Count = 0

      return False;

   end if;

else  -- Pointer = null

   return False;

end if;

It should be replaced with the shorter, more concise, and clearer equivalent line:


return Pointer /= null and then Pointer.Count > 0;

rationale

Excessive use of returns can make code confusing and unreadable. Only use return statements where warranted. Too many returns from a subprogram may be an indicator of cluttered logic. If the application requires multiple returns, use them at the same level (i.e., as in different branches of a case statement), rather than scattered throughout the subprogram code. Some rework can often reduce the number of return statements to one and make the code more clear.

exceptions

Do not avoid return statements if it detracts from natural structure and code readability.

5.6.9 Blocks

guideline

example

with Motion;

with Accelerometer_Device;

...



   ---------------------------------------------------------------------

   function Maximum_Velocity return Motion.Velocity is



      Cumulative : Motion.Velocity := 0.0;



   begin  -- Maximum_Velocity



      -- Initialize the needed devices

      ...



      Calculate_Velocity_From_Sample_Data:

         declare

            use type Motion.Acceleration;



            Current       : Motion.Acceleration := 0.0;

            Time_Delta    : Duration;



         begin  -- Calculate_Velocity_From_Sample_Data

            for I in 1 .. Accelerometer_Device.Sample_Limit loop



               Get_Samples_And_Ignore_Invalid_Data:

                  begin

                     Accelerometer_Device.Get_Value(Current, Time_Delta);

                  exception

                     when Constraint_Error =>

                        null; -- Continue trying



                     when Accelerometer_Device.Failure =>

                        raise Accelerometer_Device_Failed;

                  end Get_Samples_And_Ignore_Invalid_Data;



               exit when Current <= 0.0; -- Slowing down



               Update_Velocity:

                  declare

                     use type Motion.Velocity;

                     use type Motion.Acceleration;



                  begin

                     Cumulative := Cumulative + Current * Time_Delta;



                  exception

                     when Constraint_Error =>

                        raise Maximum_Velocity_Exceeded;

                  end Update_Velocity;



            end loop;

         end Calculate_Velocity_From_Sample_Data;



      return Cumulative;



   end Maximum_Velocity;

   ---------------------------------------------------------------------

...

rationale

Blocks break up large segments of code and isolate details relevant to each subsection of code. Variables that are only used in a particular section of code are clearly visible when a declarative block delineates that code.

Renaming may simplify the expression of algorithms and enhance readability for a given section of code. But it is confusing when a renames clause is visually separated from the code to which it applies. The declarative region allows the renames to be immediately visible when the reader is examining code that uses that abbreviation. Guideline 5.7.1 discusses a similar guideline concerning the use clause.

Local exception handlers can catch exceptions close to the point of origin and allow them to be either handled, propagated, or converted.

5.6.10 Aggregates

guideline

example

It is better to use aggregates:

Set_Position((X, Y));

Employee_Record := (Number     => 42,

                    Age        => 51,

                    Department => Software_Engineering);

than to use consecutive assignments or temporary variables:


Temporary_Position.X := 100;

Temporary_Position.Y := 200;

Set_Position(Temporary_Position);

Employee_Record.Number     := 42;

Employee_Record.Age        := 51;

Employee_Record.Department := Software_Engineering;

rationale

Using aggregates during maintenance is beneficial. If a record structure is altered, but the corresponding aggregate is not, the compiler flags the missing field in the aggregate assignment. It would not be able to detect the fact that a new assignment statement should have been added to a list of assignment statements.

Aggregates can also be a real convenience in combining data items into a record or array structure required for passing the information as a parameter. Named component association makes aggregates more readable.

See Guideline 10.4.5 for the performance impact of aggregates.

5.7 VISIBILITY

As noted in Guideline 4.2, Ada's ability to enforce information hiding and separation of concerns through its visibility controlling features is one of the most important advantages of the language. Subverting these features, for example, by too liberal use of the use clause, is wasteful and dangerous.

5.7.1 The Use Clause

guideline

example

This is a modification of the example from Guideline 4.2.3. The effect of a use clause is localized:

----------------------------------------------------------------------------------

package Rational_Numbers is

   type Rational is private;

   function "=" (X, Y : Rational) return Boolean;

   function "/" (X, Y : Integer)  return Rational;  -- construct a rational number

   function "+" (X, Y : Rational) return Rational;

   function "-" (X, Y : Rational) return Rational;

   function "*" (X, Y : Rational) return Rational;

   function "/" (X, Y : Rational) return Rational;  -- rational division

private

   ...

end Rational_Numbers;

----------------------------------------------------------------------------------

package body Rational_Numbers is

   procedure Reduce (R : in out Rational) is . . . end Reduce;

   . . .

end Rational_Numbers;

----------------------------------------------------------------------------------

package Rational_Numbers.IO is

   ...



   procedure Put (R : in  Rational);

   procedure Get (R : out Rational);

end Rational_Numbers.IO;

----------------------------------------------------------------------------------

with Rational_Numbers;

with Rational_Numbers.IO;

with Ada.Text_IO;

procedure Demo_Rationals is

   package R_IO renames Rational_Numbers.IO;



   use type Rational_Numbers.Rational;

   use R_IO;

   use Ada.Text_IO;



   X : Rational_Numbers.Rational;

   Y : Rational_Numbers.Rational;

begin  -- Demo_Rationals

   Put ("Please input two rational numbers: ");

   Get (X);

   Skip_Line;

   Get (Y);

   Skip_Line;

   Put ("X / Y = ");

   Put (X / Y);

   New_Line;

   Put ("X * Y = ");

   Put (X * Y);

   New_Line;

   Put ("X + Y = ");

   Put (X + Y);

   New_Line;

   Put ("X - Y = ");

   Put (X - Y);

   New_Line;

end Demo_Rationals;

rationale

These guidelines allow you to maintain a careful balance between maintainability and readability. Use of the use clause may indeed make the code read more like prose text. However, the maintainer may also need to resolve references and identify ambiguous operations. In the absence of tools to resolve these references and identify the impact of changing use clauses, fully qualified names are the best alternative.

Avoiding the use clause forces you to use fully qualified names. In large systems, there may be many library units named in with clauses. When corresponding use clauses accompany the with clauses and the simple names of the library packages are omitted (as is allowed by the use clause), references to external entities are obscured and identification of external dependencies becomes difficult.

In some situations, the benefits of the use clause are clear. A standard package can be used with the obvious assumption that the reader is very familiar with those packages and that additional overloading will not be introduced.

The use type clause makes both infix and prefix operators visible without the need for renames clauses. You enhance readability with the use type clause because you can write statements using the more natural infix notation for operators. See also Guideline 5.7.2.

You can minimize the scope of the use clause by placing it in the body of a package or subprogram or by encapsulating it in a block to restrict visibility.

notes

Avoiding the use clause completely can cause problems with enumeration literals, which must then be fully qualified. This problem can be solved by declaring constants with the enumeration literals as their values, except that such constants cannot be overloaded like enumeration literals.

An argument defending the use clause can be found in Rosen (1987).

automation notes

There are tools that can analyze your Ada source code, resolve overloading of names, and automatically convert between the use clause or fully qualified names.

5.7.2 The Renames Clause

guideline

example

procedure Disk_Write (Track_Name : in     Track;

                      Item       : in     Data) renames

   System_Specific.Device_Drivers.Disk_Head_Scheduler.Transmit;

See also the example in Guideline 5.7.1, where a package-level renames clause provides an abbreviation for the package Rational_Numbers_IO.

rationale

If the renaming facility is abused, the code can be difficult to read. A renames clause can substitute an abbreviation for a qualifier or long package name locally. This can make code more readable yet anchor the code to the full name. You can use the renames clause to evaluate a complex name once or to provide a new "view" of an object (regardless of whether it is tagged). However, the use of renames clauses can often be avoided or made obviously undesirable by carefully choosing names so that fully qualified names read well.

When a subprogram body calls another subprogram without adding local data or other algorithmic content, it is more readable to have this subprogram body rename the subprogram that actually does the work. Thus, you avoid having to write code to "pass through" a subprogram call (Rationale 1995, §II.12).

The list of renaming declarations serves as a list of abbreviation definitions (see Guideline 3.1.4). As an alternative, you can rename a package at the library level to define project-wide abbreviations for packages and then with the renamed packages. Often the parts recalled from a reuse library do not have names that are as general as they could be or that match the new application's naming scheme. An interface package exporting the renamed subprograms can map to your project's nomenclature. See also Guideline 5.7.1.

The method described in the Ada Reference Manual (1995) for renaming a type is to use a subtype (see Guideline 3.4.1).

The use type clause eliminates the need for renaming infix operators. Because you no longer need to rename each operator explicitly, you avoid errors such as renaming a + to a -. See also Guideline 5.7.1.

notes

You should choose package names to be minimally meaningful, recognizing that package names will be widely used as prefixes (e.g., Pkg.Operation or Object : Pkg.Type_Name;). If you rename every package to some abbreviation, you defeat the purpose of choosing meaningful names, and it becomes hard to keep track of what all the abbreviations represent.

For upward compatibility of Ada 83 programs in an Ada 95 environment, the environment includes
library-level renamings of the Ada 83 library level packages (Ada Reference Manual 1995, §J.1). It is not recommended that you use these renamings in Ada 95 code.

5.7.3 Overloaded Subprograms

guideline

example

function Sin (Angles : in     Matrix_Of_Radians) return Matrix;

function Sin (Angles : in     Vector_Of_Radians) return Vector;

function Sin (Angle  : in     Radians)           return Small_Real;

function Sin (Angle  : in     Degrees)           return Small_Real;

rationale

Excessive overloading can be confusing to maintainers (Nissen and Wallis 1984, 65). There is also the danger of hiding declarations if overloading becomes habitual. Attempts to overload an operation may actually hide the original operation if the parameter profile is not distinct. From that point on, it is not clear whether invoking the new operation is what the programmer intended or whether the programmer intended to invoke the hidden operation and accidentally hid it.

notes

This guideline does not prohibit subprograms with identical names declared in different packages.

5.7.4 Overloaded Operators

guideline

example

function "+" (X : in     Matrix;

              Y : in     Matrix)

  return Matrix;

...

Sum := A + B;

rationale

Subverting the conventional interpretation of operators leads to confusing code.

The advantage of operator overloading is that the code can become more clear and written more compactly (and readably) when it is used. This can make the semantics simple and natural. However, it can be easy to misunderstand the meaning of an overloaded operator, especially when applied to descendants. This is especially true if the programmer has not applied natural semantics. Thus, do not use overloading if it cannot be used uniformly and if it is easily misunderstood.

notes

There are potential problems with any overloading. For example, if there are several versions of the "+" operator and a change to one of them affects the number or order of its parameters, locating the occurrences that must be changed can be difficult.

5.7.5 Overloading the Equality Operator

guideline

rationale

The predefined equality operation provided with private types depends on the data structure chosen to implement that type. If access types are used, then equality will mean the operands have the same pointer value. If discrete types are used, then equality will mean the operands have the same value. If a floating-point type is used, then equality is based on Ada model intervals (see Guideline 7.2.7). You should, therefore, redefine equality to provide the meaning expected by the client. If you implement a private type using an access type, you should redefine equality to provide a deep equality. For floating-point types, you may want to provide an equality that tests for equality within some application-dependent epsilon value.

Any assumptions about the meaning of equality for private types will create a dependency on the implementation of that type. See Gonzalez (1991) for a detailed discussion.

When the definition of "=" is provided, there is a conventional algebraic meaning implied by this symbol. As described in Baker (1991), the following properties should remain true for the equality operator:

In redefining equality, you are not required to have a result type of Standard.Boolean. The Rationale
(1995, §6.3) gives two examples where your result type is a user-defined type. In a three-valued logic abstraction, you redefine equality to return one of True, False, or Unknown. In a vector processing application, you can define a component-wise equality operator that returns a vector of Boolean values. In both these instances, you should also redefine inequality because it is not the Boolean complement of the equality function.

5.8 USING EXCEPTIONS

Ada exceptions are a reliability-enhancing language feature designed to help specify program behavior in the presence of errors or unexpected events. Exceptions are not intended to provide a general purpose control construct. Further, liberal use of exceptions should not be considered sufficient for providing full software fault tolerance (Melliar-Smith and Randell 1987).

This section addresses the issues of how and when to avoid raising exceptions, how and where to handle them, and whether to propagate them. Information on how to use exceptions as part of the interface to a unit includes what exceptions to declare and raise and under what conditions to raise them. Other issues are addressed in the guidelines in Sections 4.3 and 7.5.

5.8.1 Handling Versus Avoiding Exceptions

guideline

rationale

In many cases, it is possible to detect easily and efficiently that an operation you are about to perform would raise an exception. In such a case, it is a good idea to check rather than allowing the exception to be raised and handling it with an exception handler. For example, check each pointer for null when traversing a linked list of records connected by pointers. Also, test an integer for 0 before dividing by it, and call an interrogative function Stack_Is_Empty before invoking the pop procedure of a stack package. Such tests are appropriate when they can be performed easily and efficiently as a natural part of the algorithm being implemented.

However, error detection in advance is not always so simple. There are cases where such a test is too expensive or too unreliable. In such cases, it is better to attempt the operation within the scope of an exception handler so that the exception is handled if it is raised. For example, in the case of a linked list implementation of a list, it is very inefficient to call a function Entry_Exists before each call to the procedure Modify_Entry simply to avoid raising the exception Entry_Not_Found. It takes as much time to search the list to avoid the exception as it takes to search the list to perform the update. Similarly, it is much easier to attempt a division by a real number within the scope of an exception handler to handle numeric overflow than to test, in advance, whether the dividend is too large or the divisor too small for the quotient to be representable on the machine.

In concurrent situations, tests done in advance can also be unreliable. For example, if you want to modify an existing file on a multiuser system, it is safer to attempt to do so within the scope of an exception handler than to test in advance whether the file exists, whether it is protected, whether there is room in the file system for the file to be enlarged, etc. Even if you tested for all possible error conditions, there is no guarantee that nothing would change after the test and before the modification operation. You still need the exception handlers, so the advance testing serves no purpose.

Whenever such a case does not apply, normal and predictable events should be handled by the code without the abnormal transfer of control represented by an exception. When fault handling and only fault handling code is included in exception handlers, the separation makes the code easier to read. The reader can skip all the exception handlers and still understand the normal flow of control of the code. For this reason, exceptions should never be raised and handled within the same unit, as a form of a goto statement to exit from a loop, if, case, or block statement.

Evaluating an abnormal object results in erroneous execution (Ada Reference Manual 1995, §13.9.1). The failure of a language-defined check raises an exception. In the corresponding exception handler, you want to perform appropriate cleanup actions, including logging the error (see the discussion on exception occurrences in Guideline 5.8.2) and/or reraising the exception. Evaluating the object that put you into the exception handling code will lead to erroneous execution, where you do not know whether your exception handler has executed completely or correctly. See also Guideline 5.9.1, which discusses abnormal objects in the context of Ada.Unchecked_Conversion.

5.8.2 Handlers for Others

guideline

example

The following simplified example gives the user one chance to enter an integer in the range 1 to 3. In the event of an error, it provides information back to the user. For an integer value that is outside the expected range, the function reports the name of the exception. For any other error, the function provides more complete traceback information. The amount of traceback information is implementation dependent.

with Ada.Exceptions;

with Ada.Text_IO;

with Ada.Integer_Text_IO;

function Valid_Choice return Positive is

   subtype Choice_Range is Positive range 1..3;



   Choice : Choice_Range;

begin

   Ada.Text_IO.Put ("Please enter your choice: 1, 2, or 3: ");

   Ada.Integer_Text_IO.Get (Choice);

   if Choice in Choice_Range then   -- else garbage returned

      return Choice;

   end if;

   when Out_of_Bounds : Constraint_Error => 

      Ada.Text_IO.Put_Line ("Input choice not in range.");

      Ada.Text_IO.Put_Line (Ada.Exceptions.Exception_Name (Out_of_Bounds));

      Ada.Text_IO.Skip_Line;

   when The_Error : others =>

      Ada.Text_IO.Put_Line ("Unexpected error.");

      Ada.Text_IO.Put_Line (Ada.Exceptions.Exception_Information (The_Error));

      Ada.Text_IO.Skip_Line;

end Valid_Choice;

rationale

The predefined package Ada.Exceptions allows you to log an exception, including its name and traceback information. When writing a handler for others, you should provide information about the exception to facilitate debugging. Because you can access information about an exception occurrence, you can save information suitable for later analysis in a standard way. By using exception occurrences, you can identify the particular exception and either log the details or take corrective action.

Providing a handler for others allows you to follow the other guidelines in this section. It affords a place to catch and convert truly unexpected exceptions that were not caught by the explicit handlers. While it may be possible to provide "fire walls" against unexpected exceptions being propagated without providing handlers in every block, you can convert the unexpected exceptions as soon as they arise. The others handler cannot discriminate between different exceptions, and, as a result, any such handler must treat the exception as a disaster. Even such a disaster can still be converted into a user-defined exception at that point. Because a handler for others catches any exception not otherwise handled explicitly, one placed in the frame of a task or of the main subprogram affords the opportunity to perform final cleanup and to shut down cleanly.

Programming a handler for others requires caution. You should name it in the handler (e.g.,
Error : others;) to discriminate either which exception was actually raised or precisely where it was raised. In general, the others handler cannot make any assumptions about what can be or even what needs to be "fixed."

The use of handlers for others during development, when exception occurrences can be expected to be frequent, can hinder debugging unless you take advantage of the facilities in Ada.Exceptions. It is much more informative to the developer to see a traceback with the actual exception information as captured by the Ada.Exceptions subprograms. Writing a handler without these subprograms limits the amount of error information you may see. For example, you may only see the converted exception in a traceback that does not list the point where the original exception was raised.

notes

It is possible, but not recommended, to use Exception_Id to distinguish between different exceptions in an others handler. The type Exception_Id is implementation defined. Manipulating values of type Exception_Id reduces the portability of your program and makes it harder to understand.

5.8.3 Propagation

guideline

rationale

The statement that "it can never happen" is not an acceptable programming approach. You must assume it can happen and be in control when it does. You should provide defensive code routines for the "cannot get here" conditions.

Some existing advice calls for catching and propagating any exception to the calling unit. This advice can stop a program. You should catch the exception and propagate it or a substitute only if your handler is at the wrong abstraction level to effect recovery. Effecting recovery can be difficult, but the alternative is a program that does not meet its specification.

Making an explicit request for termination implies that your code is in control of the situation and has determined that to be the only safe course of action. Being in control affords opportunities to shut down in a controlled manner (clean up loose ends, close files, release surfaces to manual control, sound alarms) and implies that all available programmed attempts at recovery have been made.

5.8.4 Localizing the Cause of an Exception

guideline

example

See Guideline 5.6.9.

rationale

In an exception handler, it is very difficult to determine exactly which statement and which operation within that statement raised an exception, particularly the predefined and implementation-defined exceptions. The predefined and implementation-defined exceptions are candidates for conversion and propagation to higher abstraction levels for handling there. User-defined exceptions, being more closely associated with the application, are better candidates for recovery within handlers.

User-defined exceptions can also be difficult to localize. Associating handlers with small blocks of code helps to narrow the possibilities, making it easier to program recovery actions. The placement of handlers in small blocks within a subprogram or task body also allows resumption of the subprogram or task after the recovery actions. If you do not handle exceptions within blocks, the only action available to the handlers is to shut down the task or subprogram as prescribed in Guideline 5.8.3.

As discussed in Guideline 5.8.2, you can log run-time system information about the exception. You can also attach a message to the exception. During code development, debugging, and maintenance, this information should be useful to localize the cause of the exception.

notes

The optimal size for the sections of code you choose to protect by a block and its exception handlers is very application-dependent. Too small a granularity forces you to expend more effort in programming for abnormal actions than for the normal algorithm. Too large a granularity reintroduces the problems of determining what went wrong and of resuming normal flow.

5.9 ERRONEOUS EXECUTION AND BOUNDED ERRORS

Ada 95 introduces the category of bounded errors. Bounded errors are cases where the behavior is not deterministic but falls within well-defined bounds (Rationale 1995, §1.4). The consequence of a bounded error is to limit the behavior of compilers so that an Ada environment is not free to do whatever it wants in the presence of errors. The Ada Reference Manual (1995) defines a set of possible outcomes for the consequences of undefined behavior, as in an uninitialized value or a value outside the range of its subtype. For example, the executing program may raise the predefined exception Program_Error, Constraint_Error, or it may do nothing.

An Ada program is erroneous when it generates an error that is not required to be detected by the compiler or > run-time environments. As stated in the Ada Reference Manual (1995, §1.1.5), "The effects of erroneous execution are unpredictable." If the compiler does detect an instance of an erroneous program, its options are to indicate a compile time error; to insert the code to raise Program_Error, possibly to write a message to that effect; or to do nothing at all.

Erroneousness is not a concept unique to Ada. The guidelines below describe or explain some specific instances of erroneousness defined in the Ada Reference Manual (1995). These guidelines are not intended to be all-inclusive but rather emphasize some commonly overlooked problem areas. Arbitrary order dependencies are not, strictly speaking, a case of erroneous execution; thus, they are discussed in Guideline 7.1.9 as a portability issue.

5.9.1 Unchecked Conversion

guideline

example

The following example shows how to use the 'Valid attribute to check validity of scalar data:

------------------------------------------------------------------------

with Ada.Unchecked_Conversion;

with Ada.Text_IO;

with Ada.Integer_Text_IO;



procedure Test is



   type Color is (Red, Yellow, Blue);

   for Color'Size use Integer'Size;



   function Integer_To_Color is

      new Ada.Unchecked_Conversion (Source => Integer,

                                    Target => Color);



   Possible_Color : Color;

   Number         : Integer;



begin  -- Test



   Ada.Integer_Text_IO.Get (Number);

   Possible_Color := Integer_To_Color (Number);



   if Possible_Color'Valid then

      Ada.Text_IO.Put_Line(Color'Image(Possible_Color));

   else

      Ada.Text_IO.Put_Line("Number does not correspond to a color.");

   end if;



end Test;

------------------------------------------------------------------------

rationale

An unchecked conversion is a bit-for-bit copy without regard to the meanings attached to those bits and bit positions by either the source or the destination type. The source bit pattern can easily be meaningless in the context of the destination type. Unchecked conversions can create values that violate type constraints on subsequent operations. Unchecked conversion of objects mismatched in size has implementation-dependent results.

Using the 'Valid attribute on scalar data allows you to check whether it is in range without raising an exception if it is out of range. There are several cases where such a validity check enhances the readability and maintainability of the code:

An access value should not be assumed to be correct when obtained without compiler or run-time checks. When dealing with access values, use of the 'Valid attribute helps prevent the erroneous dereferencing that might occur after using Ada.Unchecked_Deallocation, Unchecked_Access, or Ada.Unchecked_Conversion.

In the case of a nonscalar object used as an actual parameter in an unchecked conversion, you should ensure that its value on return from the procedure properly represents a value in the subtype. This case occurs when the parameter is of mode out or in out. It is important to check the value when interfacing to foreign languages or using a language-defined input procedure. The Ada Reference Manual (1995, §13.9.1) lists the full rules concerning data validity.

5.9.2 Unchecked Deallocation

guideline

rationale

Most of the reasons for using Ada.Unchecked_Deallocation with caution have been given in Guideline 5.4.5. When this feature is used, no checking is performed to verify that there is only one access path to the storage being deallocated. Thus, any other access paths are not made null. Depending on the value of these other access paths could result in erroneous execution.

If your Ada environment implicitly uses dynamic heap storage but does not fully and reliably reclaim and reuse heap storage, you should not use Ada.Unchecked_Deallocation.

5.9.3 Unchecked Access

guideline

rationale

The accessibility rules are checked statically at compile time (except for access parameters, which are checked dynamically). These rules ensure that the access value cannot outlive the object it designates. Because these rules are not applied in the case of Unchecked_Access, an access path could be followed to an object no longer in scope.

Isolating the use of the attribute Unchecked_Access means to isolate its use from clients of the package. You should not apply it to an access value merely for the sake of returning a now unsafe value to clients.

When you use the attribute Unchecked_Access, you are creating access values in an unsafe manner. You run the risk of dangling references, which in turn lead to erroneous execution (Ada Reference Manual
1995, §13.9.1).

exceptions

The Ada Reference Manual (1995, §13.10) defines the following potential use for this otherwise dangerous attribute. "This attribute is provided to support the situation where a local object is to be inserted into a global linked data structure, when the programmer knows that it will always be removed from the data structure prior to exiting the object's scope."

5.9.4 Address Clauses

guideline

example

Single_Address : constant System.Address := System.Storage_Elements.To_Address(...);

Interrupt_Vector_Table : Hardware_Array;

for Interrupt_Vector_Table'Address use Single_Address;

rationale

The result of specifying a single address for multiple objects or program units is undefined, as is specifying multiple addresses for a single object or program unit. Specifying multiple address clauses for an interrupt is also undefined. It does not necessarily overlay objects or program units, or associate a single entry with more than one interrupt.

You are responsible for ensuring the validity of an address you specify. Ada requires that the object of an address be an integral multiple of its alignment.

In Ada 83 (Ada Reference Manual 1983) you had to use values of type System.Address to attach an interrupt entry to an interrupt. While this technique is allowed in Ada 95, you are using an obsolete feature. You should use a protected procedure and the appropriate pragmas (Rationale 1995, §C.3.2).

5.9.5 Suppression of Exception Check

guideline

rationale

If you disable exception checks and program execution results in a condition in which an exception would otherwise occur, the program execution is erroneous. The results are unpredictable. Further, you must still be prepared to deal with the suppressed exceptions if they are raised in and propagated from the bodies of subprograms, tasks, and packages you call.

By minimizing the code that has exception checking removed, you increase the reliability of the program. There is a rule of thumb that suggests that 20% of the code is responsible for 80% of the CPU time. So, once you have identified the code that actually needs exception checking removed, it is wise to isolate it in a block (with appropriate comments) and leave the surrounding code with exception checking in effect.

5.9.6 Initialization

guideline

example

The first example illustrates the potential problem with initializing access values:

procedure Mix_Letters (Of_String : in out String) is

   type String_Ptr is access String;

   Ptr : String_Ptr := new String'(Of_String);  -- could raise Storage_Error in caller

begin -- Mix_Letters

   ...

exception

   ...  -- cannot trap Storage_Error raised during elaboration of Ptr declaration

end Mix_Letters;

The second example illustrates the issue of ensuring the elaboration of an entity before its use:


------------------------------------------------------------------------

package Robot_Controller is

   ...

   function Sense return Position;

   ...

end Robot_Controller;

------------------------------------------------------------------------

package body Robot_Controller is

...

   Goal : Position := Sense;       -- This raises Program_Error

   ...

   ---------------------------------------------------------------------

   function Sense return Position is

   begin

      ...

   end Sense;

   ---------------------------------------------------------------------

begin  -- Robot_Controller

   Goal := Sense;                  -- The function has been elaborated.

   ...

end Robot_Controller;

------------------------------------------------------------------------

rationale

Ada does not define an initial default value for objects of any type other than access types, whose initial default value is null. If you are initializing an access value at the point at which it is declared and the allocation raises the exception Storage_Error, the exception is raised in the calling not the called procedure. The caller is unprepared to handle this exception because it knows nothing about the problem-causing allocation.

Operating systems differ in what they do when they allocate a page in memory: one operating system may zero out the entire page; a second may do nothing. Therefore, using the value of an object before it has been assigned a value causes unpredictable (but bounded) behavior, possibly raising an exception. Objects can be initialized implicitly by declaration or explicitly by assignment statements. Initialization at the point of declaration is safest as well as easiest for maintainers. You can also specify default values for components of records as part of the type declarations for those records.

Ensuring initialization does not imply initialization at the declaration. In the example above, Goal must be initialized via a function call. This cannot occur at the declaration because the function Sense has not yet been elaborated, but it can occur later as part of the sequence of statements of the body of the enclosing package.

An unelaborated function called within a declaration (initialization) raises the exception, Program_Error, that must be handled outside of the unit containing the declarations. This is true for any exception the function raises even if it has been elaborated.

If an exception is raised by a function call in a declaration, it is not handled in that immediate scope. It is raised to the enclosing scope. This can be controlled by nesting blocks.

See also Guideline 9.2.3.

notes

Sometimes, elaboration order can be dictated with pragma Elaborate_All. Pragma Elaborate_All applied to a library unit causes the elaboration of the transitive closure of the unit and its dependents. In other words, all bodies of library units reachable from this library unit's body are elaborated, preventing an access-before-elaboration error (Rationale 1995, §10.3). Use the pragma Elaborate_Body when you want the body of a package to be elaborated immediately after its declaration.

5.9.7 Direct_IO and Sequential_IO

guideline

rationale

The exception Data_Error can be propagated by the Read procedures found in these packages if the element read cannot be interpreted as a value of the required subtype (Ada Reference Manual 1995, §A.13). However, if the associated check is too complex, an implementation need not propagate Data_Error. In cases where the element read cannot be interpreted as a value of the required subtype but Data_Error is not propagated, the resulting value can be abnormal, and subsequent references to the value can lead to erroneous execution.

notes

It is sometimes difficult to force an optimizing compiler to perform the necessary checks on a value that the compiler believes is in range. Most compiler vendors allow the option of suppressing optimization, which can be helpful.

5.9.8 Exception Propagation

guideline

rationale

Using Finalize or Adjust to propagate an exception results in a bounded error (Ada Reference Manual 1995, §7.6.1). Either the exception will be ignored or a Program_Error exception will be raised.

5.9.9 Protected Objects

guideline

rationale

The Ada Reference Manual (1995, §9.5.1) lists the potentially blocking operations:

Invoking any of these potentially blocking operations could lead either to a bounded error being detected or to a deadlock situation. In the case of bounded error, the exception Program_Error is raised. In addition, avoid calling routines within a protected entry, procedure, or function that could directly or indirectly invoke operating system primitives or similar operations that can cause blocking that is not visible to the Ada run-time system.

5.9.10 Abort Statement

guideline

rationale

An abort-deferred operation is one of the following:

The Ada Reference Manual (1995, §9.8) states that the practices discouraged in the guidelines result in bounded error. The exception Program_Error is raised if the implementation detects the error. If the implementation does not detect the error, the operations proceed as they would outside an abort-deferred operation. An abort statement itself may have no effect.

5.10 SUMMARY

optional parts of the syntax

parameter lists

types

data structures

expressions

statements

visibility

using exceptions

erroneous execution and bounded errors