Ada 95 Quality and Style Guide                      Chapter 5

CHAPTER 5
Programming Practices

CHAPTER 5 Programming Practices
5.1 OPTIONAL PARTS OF THE SYNTAX
5.2 PARAMETER LISTS
5.3 TYPES
5.4 DATA STRUCTURES
5.5 EXPRESSIONS
5.6 STATEMENTS
5.7 VISIBILITY
5.8 USING EXCEPTIONS
5.9 ERRONEOUS EXECUTION AND BOUNDED ERRORS
5.10 SUMMARY

Software is always subject to change. The need for this change, euphemistically known as "maintenance" arises from a variety of sources. Errors need to be corrected as they are discovered. System functionality may need to be enhanced in planned or unplanned ways. Inevitably, the requirements change over the lifetime of the system, forcing continual system evolution. Often, these modifications are conducted long after the software was originally written, usually by someone other than the original author.

Easy and successful modification requires that the software be readable, understandable, and structured according to accepted practice. If a software component cannot be easily understood by a programmer who is familiar with its intended function, that software component is not maintainable. Techniques that make code readable and comprehensible enhance its maintainability. Previous chapters presented techniques such as consistent use of naming conventions, clear and well-organized commentary, and proper modularization. This chapter presents consistent and logical use of language features.

Correctness is one aspect of reliability. While style guidelines cannot enforce the use of correct algorithms, they can suggest the use of techniques and language features known to reduce the number or likelihood of failures. Such techniques include program construction methods that reduce the likelihood of errors or that improve program predictability by defining behavior in the presence of errors.

5.1 OPTIONAL PARTS OF THE SYNTAX

Parts of the Ada syntax, while optional, can enhance the readability of the code. The guidelines given below concern use of some of these optional features.

5.1.1 Loop Names

guideline

example

Process_Each_Page:

   loop

      Process_All_The_Lines_On_This_Page:

         loop

            ...

            exit Process_All_The_Lines_On_This_Page when Line_Number = Max_Lines_On_Page;

            ...

            Look_For_Sentinel_Value:

               loop

                  ...

                  exit Look_For_Sentinel_Value when Current_Symbol = Sentinel;

                  ...

               end loop Look_For_Sentinel_Value;

            ...

         end loop Process_All_The_Lines_On_This_Page;

      ...

      exit Process_Each_Page when Page_Number = Maximum_Pages;

      ...

   end loop Process_Each_Page;

rationale

When you associate a name with a loop, you must include that name with the associated end for that loop (Ada Reference Manual 1995). This helps readers find the associated end for any given loop. This is especially true if loops are broken over screen or page boundaries. The choice of a good name for the loop documents its purpose, reducing the need for explanatory comments. If a name for a loop is very difficult to choose, this could indicate a need for more thought about the algorithm.

Regularly naming loops helps you follow Guideline 5.1.3. Even in the face of code changes, for example, adding an outer or inner loop, the exit statement does not become ambiguous.

It can be difficult to think up a name for every loop; therefore, the guideline specifies nested loops. The benefits in readability and second thought outweigh the inconvenience of naming the loops.

5.1.2 Block Names

guideline

example

Trip:

   declare

      ...

   begin  -- Trip

      Arrive_At_Airport:

         declare

            ...

         begin  -- Arrive_At_Airport

            Rent_Car;

            Claim_Baggage;

            Reserve_Hotel;

            ...

         end Arrive_At_Airport;

      Visit_Customer:

         declare

            ...

         begin  -- Visit_Customer

            -- again a set of activities...

            ...

         end Visit_Customer;

      Departure_Preparation:

         declare

            ...

         begin  -- Departure_Preparation

            Return_Car;

            Check_Baggage;

            Wait_For_Flight;

            ...

         end Departure_Preparation;

      Board_Return_Flight;

   end Trip;

rationale

When there is a nested block structure, it can be difficult to determine which end corresponds to which block. Naming blocks alleviates this confusion. The choice of a good name for the block documents its purpose, reducing the need for explanatory comments. If a name for the block is very difficult to choose, this could indicate a need for more thought about the algorithm.

This guideline is also useful if nested blocks are broken over a screen or page boundary.

It can be difficult to think up a name for each block; therefore, the guideline specifies nested blocks. The benefits in readability and second thought outweigh the inconvenience of naming the blocks.

5.1.3 Exit Statements

guideline

example

See the example in Guideline 5.1.1.

rationale

An exit statement is an implicit goto. It should specify its source explicitly. When there is a nested loop structure and an exit statement is used, it can be difficult to determine which loop is being exited. Also, future changes that may introduce a nested loop are likely to introduce an error, with the exit accidentally exiting from the wrong loop. Naming loops and their exits alleviates this confusion. This guideline is also useful if nested loops are broken over a screen or page boundary.

5.1.4 Naming End Statements

guideline

example

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

package Autopilot is

   function Is_Engaged return Boolean;

   procedure Engage;

   procedure Disengage;

end Autopilot;

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

package body Autopilot is

   ...

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

   task Course_Monitor is

      entry Reset (Engage : in     Boolean);

   end Course_Monitor;

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

   function Is_Engaged return Boolean is

   ...

   end Is_Engaged;

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

   procedure Engage is

   ...

   end Engage;

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

   procedure Disengage is

   ...

   end Disengage;

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

   task body Course_Monitor is

   ...

         accept Reset (Engage : in     Boolean) do

            ...

         end Reset;

   ...

   end Course_Monitor;

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

end Autopilot;

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

rationale

Repeating names on the end of these compound statements ensures consistency throughout the code. In addition, the named end provides a reference for the reader if the unit spans a page or screen boundary or if it contains a nested unit.

5.2 PARAMETER LISTS

A subprogram or entry parameter list is the interface to the abstraction implemented by the subprogram or entry. It is important that it is clear and that it is expressed in a consistent style. Careful decisions about formal parameter naming and ordering can make the purpose of the subprogram easier to understand, which can make it easier to use.

5.2.1 Formal Parameters

guideline

example

List_Manager.Insert (Element     => New_Employee,

                     Into_List   => Probationary_Employees,

                     At_Position => 1);

rationale

Following the variable naming guidelines (Guidelines 3.2.1 and 3.2.3) for formal parameters can make calls to subprograms read more like regular prose, as shown in the example above, where no comments are necessary. Descriptive names of this sort can also make the code in the body of the subprogram more clear.

5.2.2 Named Association

guideline

instantiation

example

Encode_Telemetry_Packet (Source         => Power_Electronics,

                         Content        => Temperature,

                         Value          => Read_Temperature_Sensor(Power_Electronics),

                         Time           => Current_Time,

                         Sequence       => Next_Packet_ID,

                         Vehicle        => This_Spacecraft,

                         Primary_Module => True);

rationale

Calls of infrequently used subprograms or entries with many formal parameters can be difficult to understand without referring to the subprogram or entry code. Named parameter association can make these calls more readable.

When the formal parameters have been named appropriately, it is easier to determine exactly what purpose the subprogram serves without looking at its code. This reduces the need for named constants that exist solely to make calls more readable. It also allows variables used as actual parameters to be given names indicating what they are without regard to why they are being passed in a call. An actual parameter, which is an expression rather than a variable, cannot be named otherwise.

Named association allows subprograms to have new parameters inserted with minimal ramifications to existing calls.

notes

The judgment of when named parameter association improves readability is subjective. Certainly, simple or familiar subprograms, such as a swap routine or a sine function, do not require the extra clarification of named association in the procedure call.

caution

A consequence of named parameter association is that the formal parameter names may not be changed without modifying the text of each call.

5.2.3 Default Parameters

guideline

example

Annex A of the Ada Reference Manual (1995) contains many examples of this practice.

rationale

Often, the majority of uses of a subprogram or entry need the same value for a given parameter. Providing that value, as the default for the parameter, makes the parameter optional on the majority of calls. It also allows the remaining calls to customize the subprogram or entry by providing different values for that parameter.

Placing default parameters at the end of the formal parameter list allows the caller to use positional association on the call; otherwise, defaults are available only when named association is used.

Often during maintenance activities, you increase the functionality of a subprogram or entry. This requires more parameters than the original form for some calls. New parameters may be required to control this new functionality. Give the new parameters default values that specify the old functionality. Calls needing the old functionality need not be changed; they take the defaults. This is true if the new parameters are added to the end of the parameter list, or if named association is used on all calls. New calls needing the new functionality can specify that by providing other values for the new parameters.

This enhances maintainability in that the places that use the modified routines do not themselves have to be modified, while the previous functionality levels of the routines are allowed to be "reused."

exceptions

Do not go overboard. If the changes in functionality are truly radical, you should be preparing a separate routine rather than modifying an existing one. One indicator of this situation would be that it is difficult to determine value combinations for the defaults that uniquely and naturally require the more restrictive of the two functions. In such cases, it is better to go ahead with creation of a separate routine.

5.2.4 Mode Indication

guideline

example

procedure Open_File (File_Name   : in     String;

                     Open_Status :    out Status_Codes);

entry Acquire (Key      : in     Capability;

               Resource :    out Tape_Drive);

rationale

By showing the mode of parameters, you aid the reader. If you do not specify a parameter mode, the default mode is in. Explicitly showing the mode indication of all parameters is a more assertive action than simply taking the default mode. Anyone reviewing the code later will be more confident that you intended the parameter mode to be in.

Use the mode that reflects the actual use of the parameter. You should avoid the tendency to make all parameters in out mode because out mode parameters may be examined as well as updated.

exceptions

It may be necessary to consider several alternative implementations for a given abstraction. For example, a bounded stack can be implemented as a pointer to an array. Even though an update to the object being pointed to does not require changing the pointer value itself, you may want to consider making the mode in out to allow changes to the implementation and to document more accurately what the operation is doing. If you later change the implementation to a simple array, the mode will have to be in out, potentially causing changes to all places that the routine is called.

5.3 TYPES

In addition to determining the possible values for variables and subtype names, type distinctions can be very valuable aids in developing safe, readable, and understandable code. Types clarify the structure of your data and can limit or restrict the operations performed on that data. "Keeping types distinct has been found to be a very powerful means of detecting logical mistakes when a program is written and to give valuable assistance whenever the program is being subsequently maintained" (Pyle 1985). Take advantage of Ada's strong typing capability in the form of subtypes, derived types, task types, protected types, private types, and limited private types.

The guidelines encourage much code to be written to ensure strong typing. While it might appear that there would be execution penalties for this amount of code, this is usually not the case. In contrast to other conventional languages, Ada has a less direct relationship between the amount of code that is written and the size of the resulting executable program. Most of the strong type checking is performed at compilation time rather than execution time, so the size of the executable code is not greatly affected.

For guidelines on specific kinds of data structures and tagged types, see Guidelines 5.4 and 9.2.1, respectively.

5.3.1 Derived Types and Subtypes

guideline

example

Type Table is a building block for the creation of new types:

type Table is

   record

      Count : List_Size  := Empty;

      List  : Entry_List := Empty_List;

   end record;

type Telephone_Directory  is new Table;

type Department_Inventory is new Table;

The following are distinct types that cannot be intermixed in operations that are not programmed explicitly to use them both:


type Dollars is new Number;

type Cents   is new Number;

Below, Source_Tail has a value outside the range of Listing_Paper when the line is empty. All the indices can be mixed in expressions, as long as the results fall within the correct subtypes:


type Columns          is range First_Column - 1 .. Listing_Width + 1;



subtype Listing_Paper is Columns range First_Column .. Listing_Width;

subtype Dumb_Terminal is Columns range First_Column .. Dumb_Terminal_Width;

type Line             is array (Columns range <>) of Bytes;

subtype Listing_Line  is Line (Listing_Paper);

subtype Terminal_Line is Line (Dumb_Terminal);

Source_Tail : Columns       := Columns'First;

Source      : Listing_Line;

Destination : Terminal_Line;

...

Destination(Destination'First .. Source_Tail - Destination'Last) :=

      Source(Columns'Succ(Destination'Last) .. Source_Tail);

rationale

The name of a derived type can make clear its intended use and avoid proliferation of similar type definitions. Objects of two derived types, even though derived from the same type, cannot be mixed in operations unless such operations are supplied explicitly or one is converted to the other explicitly. This prohibition is an enforcement of strong typing.

Define new types, derived types, and subtypes cautiously and deliberately. The concepts of subtype and derived type are not equivalent, but they can be used to advantage in concert. A subtype limits the range of possible values for a type but does not define a new type.

Types can have highly constrained sets of values without eliminating useful values. Used in concert, derived types and subtypes can eliminate many flag variables and type conversions within executable statements. This renders the program more readable, enforces the abstraction, and allows the compiler to enforce strong typing constraints.

Many algorithms begin or end with values just outside the normal range. If boundary values are not compatible within subexpressions, algorithms can be needlessly complicated. The program can become cluttered with flag variables and special cases when it could just test for zero or some other sentinel value just outside normal range.

The type Columns and the subtype Listing_Paper in the example above demonstrate how to allow sentinel values. The subtype Listing_Paper could be used as the type for parameters of subprograms declared in the specification of a package. This would restrict the range of values that could be specified by the caller. Meanwhile, the type Columns could be used to store such values internally to the body of the package, allowing First_Column - 1 to be used as a sentinel value. This combination of types and subtypes allows compatibility between subtypes within subexpressions without type conversions as would happen with derived types.

The choice between type derivation and type extension depends on what kind of changes you expect to occur to objects in the type. In general, type derivation is a very simple form of inheritance: the derived types inherit the structure, operations, and values of the parent type (Rationale 1995, §4.2). Although you can add operations, you cannot augment the data structure. You can derive from either scalar or composite types.

Type extension is a more powerful form of inheritance, only applied to tagged records, in which you can augment both the type's components and operations. When the record implements an abstraction with the potential for reuse and/or extension, it is a good candidate for making it tagged. Similarly, if the abstraction is a member of a family of abstractions with well-defined variable and common properties, you should consider a tagged record.

notes

The price of the reduction in the number of independent type declarations is that subtypes and derived types change when the base type is redefined. This trickle-down of changes is sometimes a blessing and sometimes a curse. However, usually it is intended and beneficial.

5.3.2 Anonymous Types

guideline

example

Use:

type Buffer_Index is range 1 .. 80;

type Buffer       is array (Buffer_Index) of Character;

Input_Line : Buffer;

rather than:


Input_Line : array (Buffer_Index) of Character;

rationale

Although Ada allows anonymous types, they have limited usefulness and complicate program modification. For example, except for arrays, a variable of anonymous type can never be used as an actual parameter because it is not possible to define a formal parameter of the same type. Even though this may not be a limitation initially, it precludes a modification in which a piece of code is changed to a subprogram. Although you can declare the anonymous array to be aliased, you cannot use this access value as an actual parameter in a subprogram because the subprogram's formal parameter declaration requires a type mark. Also, two variables declared using the same anonymous type declaration are actually of different types.

Even though the implicit conversion of array types during parameter passing is supported in Ada, it is difficult to justify not using the type of the parameter. In most situations, the type of the parameter is visible and easily substituted in place of an anonymous array type. The use of an anonymous array type implies that the array is only being used as a convenient way to implement a collection of values. It is misleading to use an anonymous type, and then treat the variable as an object.

When you use an access parameter or access discriminant, the anonymous type is essentially declared inside the subprogram or object itself (Rationale 1995, §3.7.1). Thus, you have no way of declaring another object of the same type, and the object is treated as a constant. In the case of a self-referential data structure (see Guideline 5.4.6), you need the access parameter to be able to manipulate the data the discriminant accesses (Rationale 1995, §3.7.1).

notes

For anonymous task types, see Guideline 6.1.4.

exceptions

If you are creating a unique table, for example, the periodic table of the elements, consider using an anonymous array type.

5.3.3 Private Types

guideline

example

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

with Ada.Finalization;

package Packet_Telemetry is

   type Frame_Header is new Ada.Finalization.Controlled with private;

   type Frame_Data   is private;

   type Frame_Codes  is (Main_Bus_Voltage, Transmitter_1_Power);

   ...

private

   type Frame_Header is new Ada.Finalization.Controlled with

      record

         ...

      end record;

   -- override adjustment and finalization to get correct assignment semantics

   procedure Adjust (Object : in out Frame_Header);

   procedure Finalize (Object : in out Frame_Header);

   type Frame_Data is

      record

         ...

      end record;

   ...

end Packet_Telemetry;

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

rationale

Limited private types and private types support abstraction and information hiding better than nonprivate types. The more restricted the type, the better information hiding is served. This, in turn, allows the implementation to change without affecting the rest of the program. While there are many valid reasons to export types, it is better to try the preferred route first, loosening the restrictions only as necessary. If it is necessary for a user of the package to use a few of the restricted operations, it is better to export the operations explicitly and individually via exported subprograms than to drop a level of restriction. This practice retains the restrictions on other operations.

Limited private types have the most restricted set of operations available to users of a package. Of the types that must be made available to users of a package, as many as possible should be derived from the controlled types or limited private. Controlled types give you the ability to adjust assignment and to finalize values, so you no longer need to create limited private types to guarantee a client that assignment and equality obey deep copy/comparison semantics. Therefore, it is possible to export a slightly less restrictive type (i.e., private type that extends Ada.Finalization.Controlled) that has an adjustable assignment operator and overridable equality operator. See also Guideline 5.4.5.

The operations available to limited private types are membership tests, selected components, components for the selections of any discriminant, qualification and explicit conversion, and attributes 'Base and 'Size. Objects of a limited private type also have the attribute 'Constrained if there are discriminants. None of these operations allows the user of the package to manipulate objects in a way that depends on the structure of the type.

notes

The predefined packages Direct_IO and Sequential_IO do not accept limited private types as generic parameters. This restriction should be considered when I/O operations are needed for a type.

See Guideline 8.3.3 for a discussion of the use of private and limited private types in generic units.

5.3.4 Subprogram Access Types

guideline

example

The following example is taken from the Rationale (1995, §3.7.2):

generic

   type Float_Type is digits <>;

package Generic_Integration is

   type Integrand is access function (X : Float_Type) return Float_Type;

   function Integrate (F        : Integrand;

                       From     : Float_Type;

                       To       : Float_Type;

                       Accuracy : Float_Type := 10.0*Float_Type'Model_Epsilon)

     return Float_Type;

end Generic_Integration;

with Generic_Integration;

procedure Try_Estimate (External_Data : in     Data_Type;

                        Lower         : in     Float;

                        Upper         : in     Float;

                        Answer        :    out Float) is

   -- external data set by other means

   function Residue (X : Float) return Float is

      Result : Float;

   begin  -- Residue

      -- compute function value dependent upon external data

      return Result;

   end Residue;

   package Float_Integration is

      new Generic_Integration (Float_Type => Float);



   use Float_Integration;

begin -- Try_Estimate

   ...

   Answer := Integrate (F    => Residue'Access,

                        From => Lower,

                        To   => Upper);

end Try_Estimate;

rationale

Access-to-subprogram types allow you to create data structures that contain subprogram references. There are many uses for this feature, for instance, implementing state machines, call backs in the X Window System, iterators (the operation to be applied to each element of a list), and numerical algorithms (e.g., integration function) (Rationale 1995, §3.7.2).

You can achieve the same effect as access-to-subprogram types for dynamic selection by using abstract tagged types. You declare an abstract type with one abstract operation and then use an access-to-class-wide type to get the dispatching effect. This technique provides greater flexibility and type safety than
access-to-subprogram types (Ada Reference Manual 1995, §3.10.2).

Access-to-subprogram types are useful in implementing dynamic selection. References to the subprograms can be stored directly in the data structure. In a finite state machine, for example, a single data structure can describe the action to be taken on state transitions. Strong type checking is maintained because Ada 95 requires that the designated subprogram has the same parameter/result profile as the one specified in the subprogram access type.

See also Guideline 7.3.2.

5.4 DATA STRUCTURES

The data structuring capabilities of Ada are a powerful resource; therefore, use them to model the data as closely as possible. It is possible to group logically related data and let the language control the abstraction and operations on the data rather than requiring the programmer or maintainer to do so. Data can also be organized in a building block fashion. In addition to showing how a data structure is organized (and possibly giving the reader an indication as to why it was organized that way), creating the data structure from smaller components allows those components to be reused. Using the features that Ada provides can increase the maintainability of your code.

5.4.1 Discriminated Records

guideline

example

An object of type Name_Holder_1 could potentially hold a string whose length is Natural'Last:

type Number_List is array (Integer range <>) of Integer;



type Number_Holder_1 (Current_Length : Natural := 0) is

   record

      Numbers : Number_List (1 .. Current_Length);

   end record;

An object of type Name_Holder_2 imposes a more reasonable restriction on the length of its string component:


type    Number_List is array (Integer range <>) of Integer;

subtype Max_Numbers is Natural range 0 .. 42;



type Number_Holder_2 (Current_Length : Max_Numbers := 0) is

   record

      Numbers : Number_List (1 .. Current_Length);

   end record;

rationale

When you use the discriminant to constrain an array inside a discriminated record, the larger the range of values the discriminant can assume, the more space an object of the type might require. Although your program may compile and link, it will fail at execution when the run-time system is unable to create an object of the potential size required.

The discriminated record captures the intent of an array whose bounds may vary at run-time. A simple constrained array definition (e.g., type Number_List is array (1 .. 42) of Integer;) does not capture the intent that there are at most 42 possible numbers in the list.

5.4.2 Heterogeneous Related Data

guideline

example

type Propulsion_Method is (Sail, Diesel, Nuclear);

type Craft is

   record

      Name   : Common_Name;

      Plant  : Propulsion_Method;

      Length : Feet;

      Beam   : Feet;

      Draft  : Feet;

   end record;

type Fleet is array (1 .. Fleet_Size) of Craft;

rationale

You help the maintainer find all of the related data by gathering it into the same construct, simplifying any modifications that apply to all rather than part. This, in turn, increases reliability. Neither you nor an unknown maintainer is liable to forget to deal with all the pieces of information in the executable statements, especially if updates are done with aggregate assignments whenever possible.

The idea is to put the information a maintainer needs to know where it can be found with the minimum of effort. For example, if all information relating to a given Craft is in the same place, the relationship is clear both in the declarations and especially in the code accessing and updating that information. But, if it is scattered among several data structures, it is less obvious that this is an intended relationship as opposed to a coincidental one. In the latter case, the declarations may be grouped together to imply intent, but it may not be possible to group the accessing and updating code that way. Ensuring the use of the same index to access the corresponding element in each of several parallel arrays is difficult if the accesses are at all scattered.

If the application must interface directly to hardware, the use of records, especially in conjunction with record representation clauses, could be useful to map onto the layout of the hardware in question.

notes

It may seem desirable to store heterogeneous data in parallel arrays in what amounts to a FORTRAN-like style. This style is an artifact of FORTRAN's data structuring limitations. FORTRAN only has facilities for constructing homogeneous arrays.

exceptions

If the application must interface directly to hardware, and the hardware requires that information be distributed among various locations, then it may not be possible to use records.

5.4.3 Heterogeneous Polymorphic Data

guideline

example

An array of type Employee_List can contain pointers to part-time and full-time employees (and possibly other kinds of employees in the future):

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

package Personnel is

   type Employee  is tagged limited private;

   type Reference is access all Employee'Class;

   ...

private

   ...

end Personnel;

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

with Personnel; 

package Part_Time_Staff is

   type Part_Time_Employee is new Personnel.Employee with 

      record

         ...

      end record;

   ...

end Part_Time_Staff;

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

with Personnel;

package Full_Time_Staff is

   type Full_Time_Employee is new Personnel.Employee with

      record

         ...

      end record;

   ...

end Full_Time_Staff;

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



... 



type Employee_List is array (Positive range <>) of Personnel.Reference;



Current_Employees : Employee_List (1..10);



...



Current_Employees(1) := new Full_Time_Staff.Full_Time_Employee;

Current_Employees(2) := new Part_Time_Staff.Part_Time_Employee;

...

rationale

Polymorphism is a means of factoring out the differences among a collection of abstractions so that programs may be written in terms of the common properties. Polymorphism allows the different objects in a heterogeneous data structure to be treated the same way, based on dispatching operations defined on the root tagged type. This eliminates the need for case statements to select the processing required for each specific type. Guideline 5.6.3 discusses the maintenance impact of using case statements.

Enumeration types, variant records, and case statements are hard to maintain because the expertise on a given variant of the data type tends to be spread all over the program. When you create a tagged type hierarchy (tagged types and type extension), you can avoid the variant records, case statement, and single enumeration type that only supports the variant record discriminant. Moreover, you localize the "expertise" about the variant within the data structure by having all the corresponding primitives for a single operation call common "operation-specific" code.

See also Guideline 9.2.1 for a more detailed discussion of tagged types.

exceptions

In some instances, you may want to use a variant record approach to organize modularity around operations. For graphic output, for example, you may find it more maintainable to use variant records. You must make the tradeoff of whether adding a new operation will be less work than adding a new variant.

5.4.4 Nested Records

guideline

example

type Coordinate is

   record

      Row    : Local_Float;

      Column : Local_Float;

   end record;

type Window is

   record

      Top_Left     : Coordinate;

      Bottom_Right : Coordinate;

   end record;

rationale

You can make complex data structures understandable and comprehensible by composing them of familiar building blocks. This technique works especially well for large record types with parts that fall into natural groupings. The components factored into separately declared records, based on a common quality or purpose, correspond to a lower level of abstraction than that represented by the larger record.

When designing a complex data structure, you must consider whether type composition or type extension is the best suited technique. Type composition refers to creating a record component whose type is itself a record. You will often need a hybrid of these techniques, that is, some components you include through type composition and others you create through type extension. Type extension may provide a cleaner design if the "intermediate" records are all instances of the same abstraction family. See also Guidelines 5.4.2 and 9.2.1.

notes

A carefully chosen name for the component of the larger record that is used to select the smaller enhances readability, for example:

if Window1.Bottom_Right.Row > Window2.Top_Left.Row then . . . 

5.4.5 Dynamic Data

guideline

example

These lines show how a dangling reference might be created:

P1 := new Object;

P2 := P1;

Unchecked_Object_Deallocation(P2);

This line can raise an exception due to referencing the deallocated object:


X := P1.all;

In the following three lines, if there is no intervening assignment of the value of P1 to any other pointer, the object created on the first line is no longer accessible after the third line. The only pointer to the allocated object has been dropped:


P1 := new Object;

...

P1 := P2;

The following code shows an example of using Finalize to make sure that when an object is finalized (i.e., goes out of scope), the dynamically allocated elements are chained on a free list:


with Ada.Finalization;

package List is

   type Object is private;

   function "=" (Left, Right : Object) return Boolean;  -- element-by-element comparison

   ... -- Operations go here

private

   type Handle is access List.Object;

   type Object is new Ada.Finalization.Controlled with

      record

         Next : List.Handle;

         ... -- Useful information go here

      end record;

   procedure Adjust (L : in out List.Object);

   procedure Finalize (L : in out List.Object);

end List;

package body List is

   Free_List : List.Handle;

   ...

   procedure Adjust (L : in out List.Object) is

   begin

      L := Deep_Copy (L);

   end Adjust;

   procedure Finalize (L : in out List.Object) is

   begin

      -- Chain L to Free_List

   end Finalize;

end List;

rationale

See also Guidelines 5.9.1, 5.9.2, 6.1.5, and 6.3.2 for variations on these problems. A dynamically allocated object is an object created by the execution of an allocator (new). Allocated objects referenced by access variables allow you to generate aliases, which are multiple references to the same object. Anomalous behavior can arise when you reference a deallocated object by another name. This is called a dangling reference. Totally disassociating a still-valid object from all names is called dropping a pointer. A dynamically allocated object that is not associated with a name cannot be referenced or explicitly deallocated.

A dropped pointer depends on an implicit memory manager for reclamation of space. It also raises questions for the reader as to whether the loss of access to the object was intended or accidental.

An Ada environment is not required to provide deallocation of dynamically allocated objects. If provided, it may be provided implicitly (objects are deallocated when their access type goes out of scope), explicitly (objects are deallocated when Ada.Unchecked_Deallocation is called), or both. To increase the likelihood of the storage space being reclaimed, it is best to call Ada.Unchecked_Deallocation explicitly for each dynamically created object when you are finished using it. Calls to Ada.Unchecked_Deallocation also document a deliberate decision to abandon an object, making the code easier to read and understand. To be absolutely certain that space is reclaimed and reused, manage your own "free list." Keep track of which objects you are finished with, and reuse them instead of dynamically allocating new objects later.

The dangers of dangling references are that you may attempt to use them, thereby accessing memory that you have released to the memory manager and that may have been subsequently allocated for another purpose in another part of your program. When you read from such memory, unexpected errors may occur because the other part of your program may have previously written totally unrelated data there. Even worse, when you write to such memory you can cause errors in an apparently unrelated part of the code by changing values of variables dynamically allocated by that code. This type of error can be very difficult to find. Finally, such errors may be triggered in parts of your environment that you did not write, for example, in the memory management system itself, which may dynamically allocate memory to keep records about your dynamically allocated memory.

Keep in mind that any unreset component of a record or array can also be a dangling reference or carry a bit pattern representing inconsistent data. Components of an access type are always initialized by default to null; however, you should not rely on this default initialization. To enhance readability and maintainability, you should include explicit initialization.

Whenever you use dynamic allocation, it is possible to run out of space. Ada provides a facility (a length clause) for requesting the size of the pool of allocation space at compile time. Anticipate that you can still run out at run time. Prepare handlers for the exception Storage_Error, and consider carefully what alternatives you may be able to include in the program for each such situation.

There is a school of thought that dictates avoidance of all dynamic allocation. It is largely based on the fear of running out of memory during execution. Facilities, such as length clauses and exception handlers for Storage_Error, provide explicit control over memory partitioning and error recovery, making this fear unfounded.

When implementing a complex data structure (tree, list, sparse matrices, etc.), you often use access types. If you are not careful, you can consume all your storage with these dynamically allocated objects. You could export a deallocate operation, but it is impossible to ensure that it is called at the proper places; you are, in effect, trusting the clients. If you derive from controlled types (see Guidelines 5.3.3, 5.9.6, 8.3.1, 8.3.3, and 9.2.3 for more information), you can use finalization to deal with deallocation of dynamic data, thus avoiding storage exhaustion. User-defined storage pools give better control over the allocation policy.

A related but distinct issue is that of shared versus copy semantics: even if the data structure is implemented using access types, you do not necessarily want shared semantics. In some instances you really want := to create a copy, not a new reference, and you really want = to compare the contents, not the reference. You should implement your structure as a controlled type. If you want copy semantics, you can redefine Adjust to perform a deep copy and = to perform a comparison on the contents. You can also redefine Finalize to make sure that when an object is finalized (i.e., goes out of scope) the dynamically allocated elements are chained on a free list (or deallocated by Ada.Unchecked_Deallocation).

The implicit use of dynamic (heap) storage by an Ada program during execution poses significant risks that software failures may occur. An Ada run-time environment may use implicit dynamic (heap) storage in association with composite objects, dynamically created tasks, and catenation. Often, the algorithms used to manage the dynamic allocation and reclamation of heap storage cause fragmentation or leakage, which can lead to storage exhaustion. It is usually very difficult or impossible to recover from storage exhaustion or Storage_Error without reloading and restarting the Ada program. It would be very restrictive to avoid all uses of implicit allocation. On the other hand, preventing both explicit and implicit deallocation significantly reduces the risks of fragmentation and leakage without overly restricting your use of composite objects, access values, task objects, and catenation.

exceptions

If a composite object is large enough to be allocated on the heap, you can still declare it as an in or in out formal parameter. The guideline is meant to discourage declaring the object in an object declaration, a formal out parameter, or the value returned by a function.

You should monitor the leakage and/or fragmentation from the heap. If they become steady-state and do not continually increase during program or partition execution, you can use the constructs described in the guidelines.

5.4.6 Aliased Objects

guideline

example

package Message_Services is

   type Message_Code_Type is range 0 .. 100;

   subtype Message is String;

   function Get_Message (Message_Code: Message_Code_Type)

     return Message;

   pragma Inline (Get_Message);

end Message_Services;

package body Message_Services is

   type Message_Handle is access constant Message;

   Message_0 : aliased constant Message := "OK";

   Message_1 : aliased constant Message := "Up";

   Message_2 : aliased constant Message := "Shutdown";

   Message_3 : aliased constant Message := "Shutup";

   . . .

   type Message_Table_Type is array (Message_Code_Type) of Message_Handle;

   

   Message_Table : Message_Table_Type :=

     (0 => Message_0'Access,

      1 => Message_1'Access,

      2 => Message_2'Access,

      3 => Message_3'Access,

      -- etc.

     );

   function Get_Message (Message_Code : Message_Code_Type)

     return Message is

   begin

      return Message_Table (Message_Code).all;

   end Get_Message;

end Message_Services;

The following code fragment shows a use of aliased objects, using the attribute 'Access to implement a generic component that manages hashed collections of objects:


generic

   type Hash_Index is mod <>;

   type Object is tagged private;

   type Handle is access all Object;

   with function Hash (The_Object : in Object) return Hash_Index;

package Collection is

   function Insert (Object : in Collection.Object) return Collection.Handle;

   function Find (Object : in Collection.Object) return Collection.Handle;

   Object_Not_Found : exception;



   ...

private

   type Cell;

   type Access_Cell is access Cell;

end Collection;

package body Collection is

   type Cell is

   record

      Value : aliased Collection.Object;

      Link  : Access_Cell;

   end record;

   type Table_Type is array (Hash_Index) of Access_Cell;



   Table : Table_Type;

   -- Go through the collision chain and return an access to the useful data.

   function Find (Object : in Collection.Object;

                  Index  : in Hash_Index) return Handle is

      Current : Access_Cell := Table (Index);

   begin

      while Current /= null loop

         if Current.Value = Object then

            return Current.Value'Access;

         else

            Current := Current.Link;

         end if;

      end loop;

      raise Object_Not_Found;

   end Find;

   -- The exported one

   function Find (Object : in Collection.Object) return Collection.Handle is

      Index : constant Hash_Index := Hash (Object);

   begin

      return Find (Object, Index);

   end Find;

   ...

end Collection;

rationale

Aliasing allows the programmer to have indirect access to declared objects. Because you can update aliased objects through more than one path, you must exercise caution to avoid unintended updates. When you restrict the aliased objects to being constant, you avoid having the object unintentionally modified. In the example above, the individual message objects are aliased constant message strings so their values cannot be changed. The ragged array is then initialized with references to each of these constant strings.

Aliasing allows you to manipulate objects using indirection while avoiding dynamic allocation. For example, you can insert an object onto a linked list without dynamically allocating the space for that object (Rationale 1995, §3.7.1).

Another use of aliasing is in a linked data structure in which you try to hide the enclosing container. This is essentially the inverse of a self-referential data structure (see Guideline 5.4.7). If a package manages some data using a linked data structure, you may only want to export access values that denote the "useful" data. You can use an access-to-object to return an access to the useful data, excluding the pointers used to chain objects.

5.4.7 Access Discriminants

guideline

example

See the examples in Guidelines 8.3.6 (using access discriminants to build an iterator) and 9.5.1 (using access discriminants in multiple inheritance).

rationale

The access discriminant is essentially a pointer of an anonymous type being used as a discriminant. Because the access discriminant is of an anonymous access type, you cannot declare other objects of the type. Thus, once you initialize the discriminant, you create a "permanent" (for the lifetime of the object) association between the discriminant and the object it accesses. When you create a self-referential structure, that is, a component of the structure is initialized to point to the enclosing object, the "constant" behavior of the access discriminant provides the right behavior to help you maintain the integrity of the structure.

See also Rationale (1995, §4.6.3) for a discussion of access discriminants to achieve multiple views of an object.

See also Guideline 6.1.3 for an example of an access discriminant for a task type.

5.4.8 Modular Types

guideline

example

with Interfaces;

procedure Main is

   type Unsigned_Byte is mod 255;



   X : Unsigned_Byte;

   Y : Unsigned_Byte;

   Z : Unsigned_Byte;

   X1 : Interfaces.Unsigned_16;

begin -- Main

   Z := X or Y;  -- does not cause overflow

show example of left shift

   X1 := 16#FFFF#;

   for Counter in 1 .. 16 loop

      X1 := Interfaces.Shift_Left (Value => X1, Amount => 1);

   end loop;

end Main;

rationale

Modular types are preferred when the number of bits is known to be fewer than the number of bits in a word and/or performance is a serious concern. Boolean arrays are appropriate when the number of bits is not particularly known in advance and performance is not a serious issue. See also Guideline 10.6.3.

5.5 EXPRESSIONS

Properly coded expressions can enhance the readability and understandability of a program. Poorly coded expressions can turn a program into a maintainer's nightmare.

5.5.1 Range Values

guideline

example

type Temperature      is range All_Time_Low .. All_Time_High;

type Weather_Stations is range            1 ..  Max_Stations;

Current_Temperature : Temperature := 60;

Offset              : Temperature;

...

for I in Weather_Stations loop

   Offset := Current_Temperature - Temperature'First;

   ...

end loop;

rationale

In the example above, it is better to use Weather_Stations in the for loop than to use Weather_Stations'First .. Weather_Stations'Last or 1 .. Max_Stations because it is clearer, less error-prone, and less dependent on the definition of the type Weather_Stations. Similarly, it is better to use Temperature'First in the offset calculation than to use All_Time_Low because the code will still be correct if the definition of the subtype Temperature is changed. This enhances program reliability.

caution

When you implicitly specify ranges and attributes like this, be careful that you use the correct subtype name. It is easy to refer to a very large range without realizing it. For example, given the declarations:

type    Large_Range is new Integer;

subtype Small_Range is Large_Range range 1 .. 10;



type Large_Array is array (Large_Range) of Integer;

type Small_Array is array (Small_Range) of Integer;

then the first declaration below works fine, but the second one is probably an accident and raises an exception on most machines because it is requesting a huge array (indexed from the smallest integer to the largest one):


Array_1 : Small_Array;

Array_2 : Large_Array;

5.5.2 Array Attributes

guideline

example

subtype Name_String is String (1 .. Name_Length);

File_Path : Name_String := (others => ' ');

...

for I in File_Path'Range loop

   ...

end loop;

rationale

In the example above, it is better to use Name_String'Range in the for loop than to use Name_String_Size, Name_String'First .. Name_String'Last, or 1 .. 30 because it is clearer, less error-prone, and less dependent on the definitions of Name_String and Name_String_Size. If Name_String is changed to have a different index type or if the bounds of the array are changed, this will still work correctly. This enhances program reliability.

5.5.3 Parenthetical Expressions

guideline

example

(1.5 * X**2)/A - (6.5*X + 47.0)

2*I + 4*Y + 8*Z + C

rationale

The Ada rules of operator precedence are defined in the Ada Reference Manual (1995, §4.5) and follow the same commonly accepted precedence of algebraic operators. The strong typing facility in Ada combined with the common precedence rules make many parentheses unnecessary. However, when an uncommon combination of operators occurs, it may be helpful to add parentheses even when the precedence rules apply. The expression:

5 + ((Y ** 3) mod 10)

is clearer, and equivalent to:


5 + Y**3 mod 10

The rules of evaluation do specify left to right evaluation for operators with the same precedence level. However, it is the most commonly overlooked rule of evaluation when checking expressions for correctness.

5.5.4 Positive Forms of Logic

guideline

example

Use:

if Operator_Missing then

rather than either:


if not Operator_Found then

or:


if not Operator_Missing then

rationale

Relational expressions can be more readable and understandable when stated in a positive form. As an aid in choosing the name, consider that the most frequently used branch in a conditional construct should be encountered first.

exceptions

There are cases in which the negative form is unavoidable. If the relational expression better reflects what is going on in the code, then inverting the test to adhere to this guideline is not recommended.

5.5.5 Short Circuit Forms of the Logical Operators

guideline

example

Use:

if Y /= 0 or else (X/Y) /= 10 then

or:


if Y /= 0 then

   if (X/Y) /= 10 then

rather than either:


if Y /= 0 and (X/Y) /= 10 then

or:


if (X/Y) /= 10 then

to avoid Constraint_Error.

Use:


if Target /= null and then Target.Distance < Threshold then

rather than:


if Target.Distance < Threshold then

to avoid referencing a field in a nonexistent object.

rationale

The use of short-circuit control forms prevents a class of data-dependent errors or exceptions that can occur as a result of expression evaluation. The short-circuit forms guarantee an order of evaluation and an exit from the sequence of relational expressions as soon as the expression's result can be determined.

In the absence of short-circuit forms, Ada does not provide a guarantee of the order of expression evaluation, nor does the language guarantee that evaluation of a relational expression is abandoned when it becomes clear that it evaluates to False (for and) or True (for or).

notes

If it is important that all parts of a given expression always be evaluated, the expression probably violates Guideline 4.1.4, which limits side-effects in functions.

5.5.6 Accuracy of Operations With Real Operands

guideline

example

Current_Temperature   : Temperature :=       0.0;

Temperature_Increment : Temperature := 1.0 / 3.0;

Maximum_Temperature   : constant    :=     100.0;

...

loop

   ...

   Current_Temperature :=

         Current_Temperature + Temperature_Increment;

   ...

   exit when Current_Temperature >= Maximum_Temperature;

   ...

end loop;

rationale

Fixed- and floating-point values, even if derived from similar expressions, may not be exactly equal. The imprecise, finite representations of real numbers in hardware always have round-off errors so that any variation in the construction path or history of two real numbers has the potential for resulting in different numbers, even when the paths or histories are mathematically equivalent.

The Ada definition of model intervals also means that the use of <= is more portable than either < or =.

notes

Floating-point arithmetic is treated in Guideline 7.2.7.

exceptions

If your application must test for an exact value of a real number (e.g., testing the precision of the arithmetic on a certain machine), then the = would have to be used. But never use = on real operands as a condition to exit a loop.

5.6 STATEMENTS

Careless or convoluted use of statements can make a program hard to read and maintain even if its global structure is well organized. You should strive for simple and consistent use of statements to achieve clarity of local program structure. Some of the guidelines in this section counsel use or avoidance of particular statements. As pointed out in the individual guidelines, rigid adherence to those guidelines would be excessive, but experience has shown that they generally lead to code with improved reliability and maintainability.

5.6.1 Nesting

guideline

instantiation

- Do not nest expressions or control structures beyond a nesting level of five.

example

The following section of code:

if not Condition_1 then

   if Condition_2 then

      Action_A;

   else  -- not Condition_2

      Action_B;

   end if;

else  -- Condition_1

   Action_C;

end if;

can be rewritten more clearly and with less nesting as:


if Condition_1 then

   Action_C;

elsif Condition_2 then

   Action_A;

else  -- not (Condition_1 or Condition_2)

   Action_B;

end if;

rationale

Deeply nested structures are confusing, difficult to understand, and difficult to maintain. The problem lies in the difficulty of determining what part of a program is contained at any given level. For expressions, this is important in achieving the correct placement of balanced grouping symbols and in achieving the desired operator precedence. For control structures, the question involves what part is controlled. Specifically, is a given statement at the proper level of nesting, that is, is it too deeply or too shallowly nested, or is the given statement associated with the proper choice, for example, for if or case statements? Indentation helps, but it is not a panacea. Visually inspecting alignment of indented code (mainly intermediate levels) is an uncertain job at best. To minimize the complexity of the code, keep the maximum number of nesting levels between three and five.

notes

Ask yourself the following questions to help you simplify the code: exceptions

If deep nesting is required frequently, there may be overall design decisions for the code that should be changed. Some algorithms require deeply nested loops and segments controlled by conditional branches. Their continued use can be ascribed to their efficiency, familiarity, and time-proven utility. When nesting is required, proceed cautiously and take special care with the choice of identifiers and loop and block names.

5.6.2 Slices

guideline

example

First  : constant Index := Index'First;

Second : constant Index := Index'Succ(First);

Third  : constant Index := Index'Succ(Second);

type Vector is array (Index range <>) of Element;

subtype Column_Vector is Vector (Index);

type    Square_Matrix is array  (Index) of Column_Vector;

subtype Small_Range  is Index range First .. Third;

subtype Diagonals    is Vector (Small_Range);

type    Tri_Diagonal is array  (Index) of Diagonals;

Markov_Probabilities : Square_Matrix;

Diagonal_Data        : Tri_Diagonal;

...

-- Remove diagonal and off diagonal elements.

Diagonal_Data(Index'First)(First) := Null_Value;

Diagonal_Data(Index'First)(Second .. Third) :=

      Markov_Probabilities(Index'First)(First .. Second);

for I in Second .. Index'Pred(Index'Last) loop

   Diagonal_Data(I) :=

         Markov_Probabilities(I)(Index'Pred(I) .. Index'Succ(I));

end loop;

Diagonal_Data(Index'Last)(First .. Second) :=

      Markov_Probabilities(Index'Last)(Index'Pred(Index'Last) .. Index'Last);

Diagonal_Data(Index'Last)(Third) := Null_Value;

rationale

An assignment statement with slices is simpler and clearer than a loop and helps the reader see the intended action. See also Guideline 10.5.7 regarding possible performance issues of slice assignments versus loops.

5.6.3 Case Statements

guideline

example

type Color is (Red, Green, Blue, Purple);

Car_Color : Color := Red;

...

case Car_Color is

   when Red .. Blue => ...

   when Purple      => ...

end case;  -- Car_Color

Now consider a change in the type:


type Color is (Red, Yellow, Green, Blue, Purple);

This change may have an unnoticed and undesired effect in the case statement. If the choices had been enumerated explicitly, as when Red | Green | Blue => instead of when Red .. Blue =>, then the case statement would not have compiled. This would have forced the maintainer to make a conscious decision about what to do in the case of Yellow.

In the following example, assume that a menu has been posted, and the user is expected to enter one of the four choices. Assume that User_Choice is declared as a Character and that Terminal_IO.Get handles errors in user input. The less readable alternative with the if/elsif statement is shown after the case statement:


Do_Menu_Choices_1:

   loop

      ...

  

      case User_Choice is

         when 'A'    => Item := Terminal_IO.Get ("Item to add");

         when 'D'    => Item := Terminal_IO.Get ("Item to delete");

         when 'M'    => Item := Terminal_IO.Get ("Item to modify");

         when 'Q'    => exit Do_Menu_Choices_1;

  

         when others => -- error has already been signaled to user

            null;

      end case;

   end loop Do_Menu_Choices_1;





Do_Menu_Choices_2:

   loop

      ...



      if User_Choice = 'A' then

         Item := Terminal_IO.Get ("Item to add");



      elsif User_Choice = 'D' then

         Item := Terminal_IO.Get ("Item to delete");



      elsif User_Choice = 'M' then

         Item := Terminal_IO.Get ("Item to modify");



      elsif User_Choice = 'Q' then

         exit Do_Menu_Choices_2;



      end if;

   end loop Do_Menu_Choices_2;

rationale

All possible values for an object should be known and should be assigned specific actions. Use of an others clause may prevent the developer from carefully considering the actions for each value. A compiler warns the user about omitted values if an others clause is not used.

You may not be able to avoid the use of others in a case statement when the subtype of the case expression has many values, for example, universal_integer, Wide_Character, or Character). If your choice of values is small compared to the range of the subtype, you should consider using an if/elsif statement. Note that you must supply an others alternative when your case expression is of a generic type.

Each possible value should be explicitly enumerated. Ranges can be dangerous because of the possibility that the range could change and the case statement may not be reexamined. If you have declared a subtype to correspond to the range of interest, you can consider using this named subtype.

In many instances, case statements enhance the readability of the code. See Guideline 10.5.3 for a discussion of the performance considerations. In many implementations, case statements may be more efficient.

Type extension and dispatching ease the maintenance burden when you add a new variant to a data structure. See also Guidelines 5.4.2 and 5.4.4.

notes

Ranges that are needed in case statements can use constrained subtypes to enhance maintainability. It is easier to maintain because the declaration of the range can be placed where it is logically part of the abstraction, not buried in a case statement in the executable code:

subtype Lower_Case is Character range 'a' .. 'z';

subtype Upper_Case is Character range 'A' .. 'Z';

subtype Control    is Character range Ada.Characters.Latin_1.NUL ..

                                      Ada.Characters.Latin_1.US;

subtype Numbers    is Character range '0' .. '9';

...

case Input_Char is

   when Lower_Case => Capitalize(Input_Char);

   when Upper_Case => null;

   when Control    => raise Invalid_Input;

   when Numbers    => null;

   ...

end case;

exceptions

It is acceptable to use ranges for possible values only when the user is certain that new values will never be inserted among the old ones, as for example, in the range of ASCII characters: 'a' .. 'z'.

Chapter 5 continued