Ada 95 Quality and Style Guide                            Chapter 5

Back to Sections 5.0 through 5.6.3.

5.6.4 Loops

guideline

  • Use for loops, whenever possible.
  • Use while loops when the number of iterations cannot be calculated before entering the loop but a simple continuation condition can be applied at the top of the loop.
  • Use plain loops with exit statements for more complex situations.
  • Avoid exit statements in while and for loops.
  • Minimize the number of ways to exit a loop.

  • 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

  • Use exit statements to enhance the readability of loop termination code (NASA 1987).
  • Use exit when ... rather than if ... then exit whenever possible (NASA 1987).
  • Review exit statement placement.

  • example

    See the examples in Guidelines 5.1.1 and Guidelines 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

  • Consider specifying bounds on loops .
  • Consider specifying bounds on recursion .

  • 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

  • Do not use goto statements.

  • 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

  • Minimize the number of return statements from a subprogram (NASA 1987).
  • Highlight return statements with comments or white space to keep them from being lost in other code.

  • 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

  • Use blocks to localize the scope of declarations.
  • Use blocks to perform local renaming.
  • Use blocks to define local exception handlers.

  • 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

  • Use an aggregate instead of a sequence of assignments to assign values to all components of a record.
  • Use an aggregate instead of a temporary variable when building a record to pass as an actual parameter.
  • Use positional association only when there is a conventional ordering of the arguments.

  • 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

  • When you need to provide visibility to operators, use the use type clause.
  • Avoid/minimize the use of the use clause (Nissen and Wallis 1984).
  • Consider using a package renames clause rather than a use clause for a package.
  • Consider using the use clause in the following situations:
    - When standard packages are needed and no ambiguous references are introduced
    - When references to enumeration literals are needed
  • Localize the effect of all use clauses.

  • 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

  • Limit the scope of a renaming declaration to the minimum necessary scope.
  • Rename a long, fully qualified name to reduce the complexity if it becomes unwieldy (see Guideline 3.1.4).
  • Use renaming to provide the body of a subprogram if this subprogram merely calls the first subprogram.
  • Rename declarations for visibility purposes rather than using the use clause, except for operators (see Guideline 5.7.1).
  • Rename parts when your code interfaces to reusable components originally written with nondescriptive or inapplicable nomenclature.
  • Use a project-wide standard list of abbreviations to rename common packages.
  • Provide a use type rather than a renames clause to provide visibility to operators.

  • 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

  • Limit overloading to widely used subprograms that perform similar actions on arguments of different types (Nissen and Wallis 1984).

  • 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

  • Preserve the conventional meaning of overloaded operators (Nissen and Wallis 1984).
  • Use "+" to identify adding, joining, increasing, and enhancing kinds of functions.
  • Use "-" to identify subtraction, separation, decreasing, and depleting kinds of functions.
  • Use operator overloading sparingly and uniformly when applied to tagged types.

  • 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

  • Define an appropriate equality operator for private types.
  • Consider redefining the equality operator for a private type.
  • When overloading the equality operator for types, maintain the properties of an algebraic equivalence relation.

  • 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:

    - Reflexive: a = a
    - Symmetric: a = b ==> b = a
    - Transitive: a = b and b = c ==> a = c

    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

  • When it is easy and efficient to do so, avoid causing exceptions to be raised.
  • Provide handlers for exceptions that cannot be avoided.
  • Use exception handlers to enhance readability by separating fault handling from normal execution.
  • Do not use exceptions and exception handlers as goto statements.
  • Do not evaluate the value of an object (or a part of an object) that has become abnormal because of the failure of a language-defined check.

  • 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

  • When writing an exception handler for others, capture and return additional information about the exception through the Exception_Name, Exception_Message, or Exception_Information subprograms declared in the predefined package Ada.Exceptions.
  • Use others only to catch exceptions you cannot enumerate explicitly, preferably only to flag a potential abort.
  • During development, trap others, capture the exception being handled, and consider adding an explicit handler for that exception.

  • 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

  • Handle all exceptions, both user and predefined .
  • For every exception that might be raised, provide a handler in suitable frames to protect against undesired propagation outside the abstraction .

  • 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

  • Do not rely on being able to identify the fault-raising, predefined, or implementation-defined exceptions.
  • Use the facilities defined in Ada.Exceptions to capture as much information as possible about an exception.
  • Use blocks to associate localized sections of code with their own exception handlers.

  • 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

  • Use Ada.Unchecked_Conversion only with the utmost care (Ada Reference Manual 1995, §13.9).
  • Consider using the 'Valid attribute to check the validity of scalar data.
  • Ensure that the value resulting from Ada.Unchecked_Conversion properly represents a value of the parameter's subtype.
  • Isolate the use of Ada.Unchecked_Conversion in package bodies.

  • 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:

    - Data produced through an unchecked conversion
    - Input data
    - Parameter values returned from a foreign language interface
    - Aborted assignment (during asynchronous transfer of control or execution of an abort statement)
    - Disrupted assignment from failure of a language-defined check
    - Data whose address has been specified with the 'Address attribute

    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

  • Isolate the use of Ada.Unchecked_Deallocation in package bodies.
  • Ensure that no dangling reference to the local object exists after exiting the scope of the local object.

  • 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

  • Minimize the use of the attribute Unchecked_Access, preferably isolating it to package bodies.
  • Use the attribute Unchecked_Access only on data whose lifetime/scope is "library level."

  • 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

  • Use address clauses to map variables and entries to the hardware device or memory, not to model the FORTRAN "equivalence" feature.
  • Ensure that the address specified in an attribute definition clause is valid and does not conflict with the alignment.
  • If available in your Ada environment, use the package Ada.Interrupts to associate handlers with interrupts.
  • Avoid using the address clause for nonimported program units.

  • 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

  • Do not suppress exception checks during development.
  • If necessary, during operation, introduce blocks that encompass the smallest range of statements that can safely have exception checking removed.

  • 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

  • Initialize all objects prior to use.
  • Use caution when initializing access values.
  • Do not depend on default initialization that is not part of the language.
  • Derive from a controlled type and override the primitive procedure to ensure automatic initialization.
  • Ensure elaboration of an entity before using it.
  • Use function calls in declarations cautiously.

  • 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

  • Ensure that values obtained from Ada.Direct_IO and Ada.Sequential_IO are in range.
  • Use the 'Valid attribute to check the validity of scalar values obtained through Ada.Direct_IO and Ada.Sequential_IO.

  • 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

  • Prevent exceptions from propagating outside any user-defined Finalize or Adjust procedure by providing handlers for all predefined and user-defined exceptions at the end of each procedure.

  • 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

  • Do not invoke a potentially blocking operation within a protected entry, a protected procedure, or a protected function.

  • rationale

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

    - Select statement
    - Accept statement
    - Entry-call statement
    - Delay statement
    - Abort statement
    - Task creation or activation
    - External call on a protected subprogram (or an external requeue) with the same target object as that of the protected action
    - Call on a subprogram whose body contains a potentially blocking operation

    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

  • Do not use an asynchronous select statement within abort-deferred operations.
  • Do not create a task that depends on a master that is included entirely within the execution of an abort-deferred operation.

  • rationale

    An abort-deferred operation is one of the following:

    - Protected entry, protected procedure, or protected function
    - User-defined Initialize procedure used as the last step of a default initialization of a controlled object
    - User-defined Finalize procedure used in finalization of a controlled object
    - User-defined Adjust procedure used in assignment of a controlled object

    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

  • Associate names with loops when they are nested (Booch 1986, 1987).
  • Associate names with any loop that contains an exitstatement.
  • Associate names with blocks when they are nested .
  • Use loop names on all exit statements from nested loops.
  • Include the defining program unit name at the end of a package specification and body.
  • Include the defining identifier at the end of a task specification and body.
  • Include the entry identifier at the end of an accept statement.
  • Include the designator at the end of a subprogram body.
  • Include the defining identifier at the end of a protected unit declaration.

  • parameter lists

  • Name formal parametername formal parameters descriptively to reduce the need for comments .
  • Use named parameter association in calls of infrequently used subprograms or entries with many formal parameters .
  • Use named association when instantiating generics.
  • Use named association for clarification when the actual parameter is any literal or expression.
  • Use named association when supplying a nondefault value to an optional parameter.
  • Provide default parameters to allow for occasional, special use of widely used subprograms or entries.
  • Place default parameters at the end of the formal parameter list.
  • Consider providing default values to new parameters added to an existing subprogram.
  • Show the mode indication of all procedure and entry parameters (Nissen and Wallis 1984).
  • Use the most restrictive parameter mode applicable to your application.

  • types

  • Use existing types as building blocks by deriving new types from them.
  • Use range constraints on subtypes.
  • Define new types, especially derived types, to include the largest set of possible values, including boundary values.
  • Constrain the ranges of derived types with subtypes, excluding boundary values.
  • Use type derivation rather than type extension when there are no meaningful components to add to the type.
  • Avoid anonymous array types.
  • Use anonymous array types for array variables only when no suitable type exists or can be created and the array will not be referenced as a whole (e.g., used as a subprogram parameter).
  • Use access parameters and access discriminants to guarantee that the parameter or discriminant is treated as a constant.
  • Derive from controlled types in preference to using limited private types.
  • Use limited private types in preference to private types.
  • Use private types in preference to nonprivate types.
  • Explicitly export needed operations rather than easing restrictions.
  • Use access-to-subprogram types for indirect access to subprograms.
  • Wherever possible, use abstract tagged types and dispatching rather than access-to-subprogram types to implement dynamic selection and invocation of subprograms.

  • data structures

  • When declaring a discriminant, use as constrained a subtype as possible (i.e., subtype with as specific a range constraint as possible).
  • Use a discriminated record rather than a constrained array to represent an array whose actual values are unconstrained.
  • Use records to group heterogeneous but related data.
  • Consider records to map to I/O device data.
  • Use access types to class-wide types to implement heterogeneous polymorphic data structures.
  • Use tagged types and type extension rather than variant records (in combination with enumeration types and case statements).
  • Record structures should not always be flat. Factor out common parts.
  • For a large record structure, group related components into smaller subrecords.
  • For nested records, pick element names that read well when inner elements are referenced.
  • Consider using type extension to organize large data structures.
  • Differentiate between static and dynamic data. Use dynamically allocated objects with caution.
  • Use dynamically allocated data structures only when it is necessary to create and destroy them dynamically or to be able to reference them by different names.
  • Do not drop pointers to undeallocated objects.
  • Do not leave dangling references to deallocated objects.
  • Initialize all access variables and components within a record.
  • Do not rely on memory deallocation.
  • Deallocate explicitly.
  • Use length clauses to specify total allocation size.
  • Provide handlers for Storage_Error .
  • Use controlled types to implement private types that manipulate dynamic data.
  • Avoid unconstrained record objects unless your run-time environment reliably reclaims dynamic heap storage.
  • Unless your run-time environment reliably reclaims dynamic heap storage, declare the following items only in the outermost, unnested declarative part of either a library package, a main subprogram, or a permanent task:
    - Access types
    - Constrained composite objects with nonstatic bounds
    - Objects of an unconstrained composite type other than unconstrainedrecords
    - Composite objects large enough (at compile time) for the compiler to allocate implicitly on the heap
  • Unless your run-time environment reliably reclaims dynamic heap storage or you are creating permanent, dynamically allocated tasks, avoid declaring tasks in the following situations:
    - Unconstrained array subtypes whose components are tasks
    - Discriminated record subtypes containing a component that is an array of tasks, where the array size depends on the value of the discriminant
    - Any declarative region other than the outermost, unnested declarative part of either a library package or a main subprogram
    - Arrays of tasks that are not statically constrained
  • Minimize the use of aliased variables.
  • Use aliasing for statically created, ragged arrays (Rationale 1995, §3.7.1).
  • Use aliasing to refer to part of a data structure when you want to hide the internal connections and bookkeeping information.
  • Use access discriminants to create self-referential data structures, i.e., a data structure one of whose components points to the enclosing structure.
  • Use modular types rather than a Boolean arrays when you create data structures that need bit-wise operations, such as and and or.

  • expressions

  • Use 'First or 'Last instead of numeric literals to represent the first or last values of a range.
  • Use 'Range or the subtype name of the range instead of 'First .. 'Last.
  • Use array attributes 'First , 'Last , or 'Length instead of numeric literals for accessing arrays.
  • Use the 'Range of the array instead of the name of the index subtype to express a range.
  • Use 'Range instead of 'First .. 'Last to express a range.
  • Use parentheses to specify the order of subexpression evaluation to clarify expressions (NASA 1987).
  • Use parentheses to specify the order of evaluation for subexpressions whose correctness depends on left to right evaluation.
  • Avoid names and constructs that rely on the use of negatives .
  • Choose names of flags so they represent states that can be used in positive form.
  • Use short-circuit forms of the logical operators to specify the order of conditions when the failure of one condition means that the other condition will raise an exception.
  • Use <= and >= in relational expressions with real operands instead of =.

  • statements

  • Minimize the depth of nested expressions (Nissen and Wallis 1984).
  • Minimize the depth of nested control structures (Nissen and Wallis 1984).
  • Try using simplification heuristics.
  • Use slices rather than a loop to copy part of an array.
  • Minimize the use of an others choice in a case statement.
  • Do not use ranges of enumeration literals in case statements.
  • Use case statements rather than if/elsif statements, wherever possible.
  • Use type extension and dispatching rather than case statements, if possible.
  • Use for loops, whenever possible.
  • Use while loops when the number of iterations cannot be calculated before entering the loop but a simple continuation condition can be applied at the top of the loop.
  • Use plain loops with exit statements for more complex situations.
  • Avoid exit statements in while and for loops.
  • Minimize the number of ways to exit a loop.
  • Use exit statements to enhance the readability of loop termination code (NASA 1987).
  • Use exit when ... rather than if ... then exit whenever possible (NASA 1987).
  • Review exit statement placement.
  • Consider specifying bounds on loops.
  • Consider specifying bounds on recursion.
  • Do not use goto statements.
  • Minimize the number of return statements from a subprogram (NASA 1987).
  • Highlight return statements with comments or white space to keep them from being lost in other code.
  • Use blocks to localize the scope of declarations.
  • Use blocks to perform local renaming.
  • Use blocks to define local exception handlers.
  • Use an aggregate instead of a sequence of assignments to assign values to all components of a record
  • Use an aggregate instead of a temporary variable when building a record to pass as an actual parameter
  • Use positional association only when there is a conventional ordering of the arguments.

  • visibility

  • When you need to provide visibility to operators, use the use type clause.
  • Avoid/minimize the use of the use clause (Nissen and Wallis 1984).
  • Consider using a package renames clause rather than a use clause for a package.
  • Consider using the use clause in the following situations:
    - When standard packages are needed and no ambiguous references are introduced
    - When references to enumeration literals are needed
  • Localize the effect of all use clauses.
  • Limit the scope of a renaming declaration to the minimum necessary scope.
  • Rename a long, fully qualified name to reduce the complexity if it becomes unwieldy.
  • Use renaming to provide the body of a subprogram if this subprogram merely calls the first subprogram.
  • Rename declarations for visibility purposes rather than using the use clause, except for operators .
  • Rename parts when your code interfaces to reusable components originally written with nondescriptive or inapplicable nomenclature.
  • Use a project-wide standard list of abbreviations to rename common packages.
  • Provide a use type rather than a renames clause to provide visibility to operators.
  • Limit overloading to widely used subprograms that perform similar actions on arguments of different types (Nissen and Wallis 1984).
  • Preserve the conventional meaning of overloaded operators (Nissen and Wallis 1984).
  • Use "+" to identify adding, joining, increasing, and enhancing kinds of functions.
  • Use "-" to identify subtraction, separation, decreasing, and depleting kinds of functions.
  • Use operator overloading sparingly and uniformly when applied to tagged types.
  • Define an appropriate equality operator for private types.
  • Consider redefining the equality operator for a private type.
  • When overloading the equality operator for types, maintain the properties of an algebraic equivalence relation.

  • using exceptions

  • When it is easy and efficient to do so, avoid causing exceptions to be raised.
  • Provide handlers for exceptions that cannot be avoided.
  • Use exception handlers to enhance readability by separating fault handling from normal execution.
  • Do not use exceptions and exception handlers as goto statements.
  • Do not evaluate the value of an object (or a part of an object) that has become abnormal because of the failure of a language-defined check.
  • When writing an exception handler for others, capture and return additional information about the exception through the Exception_Name, Exception_Message, or Exception_Information subprograms declared in the predefined package Ada.Exceptions.
  • Use others only to catch exceptions you cannot enumerate explicitly, preferably only to flag a potential abort.
  • During development, trap others, capture the exception being handled, and consider adding an explicit handler for that exception.
  • Handle all exceptions, both user and predefined .
  • For every exception that might be raised, provide a handler in suitable frames to protect against undesired propagation outside the abstraction .
  • Do not rely on being able to identify the fault-raising, predefined, or implementation-defined exceptions.
  • Use the facilities defined in Ada.Exceptions to capture as much information as possible about an exception.
  • Use blocks to associate localized sections of code with their own exception handlers.

  • erroneous execution and bounded errors

  • Use Ada.Unchecked_Conversion only with the utmost care (Ada Reference Manual 1995, §13.9).
  • Consider using the 'Valid attribute to check the validity of scalar data).
  • Ensure that the value resulting from Ada.Unchecked_Conversion properly represents a value of the parameter's subtype.
  • Isolate the use of Ada.Unchecked_Conversion in package bodies.
  • Isolate the use of Ada.Unchecked_Deallocation in package bodies.
  • Ensure that no dangling reference to the local object exists after exiting the scope of the local object.
  • Minimize the use of the attribute Unchecked_Access, preferably isolating it to package bodies.
  • Use the attribute Unchecked_Access only on data whose lifetime/scope is "library level."
  • Use address clauses to map variables and entries to the hardware device or memory, not to model the FORTRAN "equivalence" feature.
  • Ensure that the address specified in an attribute definition clause is valid and does not conflict with the alignment.
  • If available in your Ada environment, use the package Ada.Interrupts to associate handlers with interrupts.
  • Avoid using the address clause for nonimported program units.
  • Do not suppress exception checks during development.
  • If necessary, during operation, introduce blocks that encompass the smallest range of statements that can safely have exception checking removed.
  • Initialize all objects , including access values, prior to use.
  • Use caution when initializing access values.
  • Do not depend on default initialization that is not part of the language.
  • Derive from a controlled type and override the primitive procedure to ensure automatic initialization.
  • Ensure elaboration of an entity before using it.
  • Use function calls in declarations cautiously.
  • Ensure that values obtained from Ada.Direct_IO and Ada.Sequential_IO are in range.
  • Use the 'Valid attribute to check the validity of scalar values obtained through Ada.Direct_IO and Ada.Sequential_IO.
  • Prevent exceptions from propagating outside any user-defined Finalize or Adjust procedure by providing handlers for all predefined and user-defined exceptions at the end of each procedure.
  • Do not invoke a potentially blocking operation within a protected entry, a protected procedure, or a protected function.
  • Do not use an asynchronous select statement within abort-deferred operations.
  • Do not create a task that depends on a master that is included entirely within the execution of an abort-deferred operation.