Ada Style Guide/Programming Practices
Introduction
[edit | edit source]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.
Optional Parts of the Syntax
[edit | edit source]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.
Loop Names
[edit | edit source]guideline
[edit | edit source]- Associate names with loops when they are nested (Booch 1986, 1987).
- Associate names with any loop that contains an exitstatement.
example
[edit | edit source]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
[edit | edit source]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.
Block Names
[edit | edit source]guideline
[edit | edit source]- Associate names with blocks when they are nested.
example
[edit | edit source]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
[edit | edit source]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.
Exit Statements
[edit | edit source]guideline
[edit | edit source]- Use loop names on all exit statements from nested loops.
example
[edit | edit source]See the example in 5.1.1 .
rationale
[edit | edit source]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.
Naming End Statements
[edit | edit source]guideline
[edit | edit source]- 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 acceptstatement.
- Include the designator at the end of a subprogram body.
- Include the defining identifier at the end of a protected unit declaration.
example
[edit | edit source]------------------------------------------------------------------------
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
[edit | edit source]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.
Parameter Lists
[edit | edit source]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.
Formal Parameters
[edit | edit source]guideline
[edit | edit source]- Name formal parameters descriptively to reduce the need for comments.
example
[edit | edit source]List_Manager.Insert (Element => New_Employee,
Into_List => Probationary_Employees,
At_Position => 1);
rationale
[edit | edit source]Following the variable naming 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.
Named Association
[edit | edit source]guideline
[edit | edit source]- 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.
instantiation
[edit | edit source]- Use named parameter association in calls of subprograms or entries called from less than five places in a single source file or with more than two formal parameters.
example
[edit | edit source]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
[edit | edit source]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
[edit | edit source]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
[edit | edit source]A consequence of named parameter association is that the formal parameter names may not be changed without modifying the text of each call.
Default Parameters
[edit | edit source]guideline
[edit | edit source]- 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.
example
[edit | edit source]Ada Reference Manual (1995) contains many examples of this practice.
rationale
[edit | edit source]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
[edit | edit source]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.
Mode Indication
[edit | edit source]guideline
[edit | edit source]- Show the mode indication of all procedure and entry parameters (Nissen and Wallis 1984 ).
- Use the most restrictive parameter mode applicable to your application.
example
[edit | edit source]procedure Open_File (File_Name : in String;
Open_Status : out Status_Codes);
entry Acquire (Key : in Capability;
Resource : out Tape_Drive);
rationale
[edit | edit source]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
[edit | edit source]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.
Types
[edit | edit source]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 9.2.1 , respectively.
Derived Types and Subtypes
[edit | edit source]guideline
[edit | edit source]- 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.
example
[edit | edit source]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
[edit | edit source]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
[edit | edit source]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.
Anonymous Types
[edit | edit source]guideline
[edit | edit source]- 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.
example
[edit | edit source]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
[edit | edit source]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
[edit | edit source]For anonymous task types, see Guideline 6.1.4 .
exceptions
[edit | edit source]If you are creating a unique table, for example, the periodic table of the elements, consider using an anonymous array type.
Private Types
[edit | edit source]guideline
[edit | edit source]- 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.
example
[edit | edit source]------------------------------------------------------------------------
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
[edit | edit source]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
[edit | edit source]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.
Subprogram Access Types
[edit | edit source]guideline
[edit | edit source]- 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.
example
[edit | edit source]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
[edit | edit source]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 [Annotated]).
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 .
Data Structures
[edit | edit source]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.
Discriminated Records
[edit | edit source]guideline
[edit | edit source]- 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.
example
[edit | edit source]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
[edit | edit source]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.
Heterogeneous Related Data
[edit | edit source]guideline
[edit | edit source]- Use records to group heterogeneous but related data.
- Consider records to map to I/O device data.
example
[edit | edit source]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
[edit | edit source]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
[edit | edit source]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
[edit | edit source]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.
Heterogeneous Polymorphic Data
[edit | edit source]guideline
[edit | edit source]- 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).
example
[edit | edit source]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
[edit | edit source]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
[edit | edit source]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.
Nested Records
[edit | edit source]guideline
[edit | edit source]- 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.
example
[edit | edit source]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
[edit | edit source]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
[edit | edit source]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 . . .
Dynamic Data
[edit | edit source]guideline
[edit | edit source]- 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 unconstrained records
- 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
example
[edit | edit source]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
[edit | edit source]See also 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 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
[edit | edit source]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.
Aliased Objects
[edit | edit source]guideline
[edit | edit source]- 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.
example
[edit | edit source]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
[edit | edit source]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.
Access Discriminants
[edit | edit source]guideline
[edit | edit source]- Use access discriminants to create self-referential data structures, i.e., a data structure one of whose components points to the enclosing structure.
example
[edit | edit source]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
[edit | edit source]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.
Modular Types
[edit | edit source]guideline
[edit | edit source]- Use modular types rather than Boolean arrays when you create data structures that need bit-wise operations, such as and and or.
example
[edit | edit source]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
[edit | edit source]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 .
Expressions
[edit | edit source]Properly coded expressions can enhance the readability and understandability of a program. Poorly coded expressions can turn a program into a maintainer's nightmare.
Range Values
[edit | edit source]guideline
[edit | edit source]- 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.
example
[edit | edit source]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
[edit | edit source]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
[edit | edit source]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;
Array Attributes
[edit | edit source]guideline
[edit | edit source]- 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.
example
[edit | edit source]subtype Name_String is String (1 .. Name_Length);
File_Path : Name_String := (others => ' ');
...
for I in File_Path'Range loop
...
end loop;
rationale
[edit | edit source]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.
Parenthetical Expressions
[edit | edit source]guideline
[edit | edit source]- 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.
example
[edit | edit source](1.5 * X**2)/A - (6.5*X + 47.0)
2*I + 4*Y + 8*Z + C
rationale
[edit | edit source]The Ada rules of operator precedence are defined in the Ada Reference Manual 1995, §4.5 [Annotated] 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.
Positive Forms of Logic
[edit | edit source]guideline
[edit | edit source]- 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.
example
[edit | edit source]Use:
if Operator_Missing then
rather than either:
if not Operator_Found then
or:
if not Operator_Missing then
rationale
[edit | edit source]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
[edit | edit source]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.
Short Circuit Forms of the Logical Operators
[edit | edit source]guideline
[edit | edit source]- 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.
example
[edit | edit source]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
[edit | edit source]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
[edit | edit source]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.
Accuracy of Operations With Real Operands
[edit | edit source]guideline
[edit | edit source]- Use <= and >= in relational expressions with real operands instead of =.
example
[edit | edit source]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
[edit | edit source]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
[edit | edit source]Floating-point arithmetic is treated in Guideline 7.2.7 .
exceptions
[edit | edit source]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 .
Statements
[edit | edit source]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.
Nesting
[edit | edit source]guideline
[edit | edit source]- 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 (see the following Notes ).
instantiation
[edit | edit source]- Do not nest expressions or control structures beyond a nesting level of five.
example
[edit | edit source]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
[edit | edit source]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
[edit | edit source]Ask yourself the following questions to help you simplify the code:
- Can some part of the expression be put into a constant or variable?
- Does some part of the lower nested control structures represent a significant and, perhaps, reusable computation that I can factor into a subprogram ?
- Can I convert these nested if statements into a case statement?
- Am I using else if where I could be using elsif ?
- Can I reorder the conditional expressions controlling this nested structure?
- Is there a different design that would be simpler?
exceptions
[edit | edit source]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.
Slices
[edit | edit source]guideline
[edit | edit source]- Use slices rather than a loop to copy part of an array.
example
[edit | edit source]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
[edit | edit source]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.
Case Statements
[edit | edit source]guideline
[edit | edit source]- 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.
example
[edit | edit source]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
[edit | edit source]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
[edit | edit source]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
[edit | edit source]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'.
Loops
[edit | edit source]guideline
[edit | edit source]- 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
[edit | edit source]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
[edit | edit source]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.
Exit Statements
[edit | edit source]guideline
[edit | edit source]- Use exit statements to enhance the readability of loop termination code (NASA 1987).
- Use exit when ... rather than if ... then exitwhenever possible (NASA 1987).
- Review exit statement placement.
example
[edit | edit source]See the examples in Guidelines 5.1.1 and Guidelines 5.6.4.
rationale
[edit | edit source]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.
Recursion and Iteration Bounds
[edit | edit source]guideline
[edit | edit source]- Consider specifying bounds on loops .
- Consider specifying bounds on recursion .
example
[edit | edit source]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
[edit | edit source]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
[edit | edit source]If a loop uses the for iteration scheme (Guideline 5.6.4), it follows this guideline.
exceptions
[edit | edit source]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.
Goto Statements
[edit | edit source]guideline
[edit | edit source]Do not use goto statements.
rationale
[edit | edit source]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
[edit | edit source]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.
Return Statements
[edit | edit source]guideline
[edit | edit source]- 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
[edit | edit source]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
[edit | edit source]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
[edit | edit source]Do not avoid return statements if it detracts from natural structure and code readability.
Blocks
[edit | edit source]guideline
[edit | edit source]- Use blocks to localize the scope of declarations.
- Use blocks to perform local renaming.
- Use blocks to define local exception handlers.
example
[edit | edit source]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
[edit | edit source]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.
Aggregates
[edit | edit source]guideline
[edit | edit source]- 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
[edit | edit source]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
[edit | edit source]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.
Visibility
[edit | edit source]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.
The Use Clause
[edit | edit source]guideline
[edit | edit source]- 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
[edit | edit source]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
[edit | edit source]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
[edit | edit source]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
[edit | edit source]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.
The Renames Clause
[edit | edit source]guideline
[edit | edit source]- 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
[edit | edit source]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
[edit | edit source]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, §8.5 [Annotated] 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
[edit | edit source]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 [Annotated]). It is not recommended that you use these renamings in Ada 95 code.
Overloaded Subprograms
[edit | edit source]guideline
[edit | edit source]Limit overloading to widely used subprograms that perform similar actions on arguments of different types (Nissen and Wallis 1984).
example
[edit | edit source]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
[edit | edit source]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
[edit | edit source]This guideline does not prohibit subprograms with identical names declared in different packages.
Overloaded Operators
[edit | edit source]guideline
[edit | edit source]- 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
[edit | edit source]function "+" (X : in Matrix;
Y : in Matrix)
return Matrix;
...
Sum := A + B;
rationale
[edit | edit source]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
[edit | edit source]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.
Overloading the Equality Operator
[edit | edit source]guideline
[edit | edit source]- 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
[edit | edit source]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.
Using exceptions
[edit | edit source]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.
Handling Versus Avoiding Exceptions
[edit | edit source]guideline
[edit | edit source]- 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
[edit | edit source]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 [Annotated]). 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.
Handlers for Others
[edit | edit source]guideline
[edit | edit source]- 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
[edit | edit source]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
[edit | edit source]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
[edit | edit source]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.
Propagation
[edit | edit source]guideline
[edit | edit source]- 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
[edit | edit source]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.
Localizing the Cause of an Exception
[edit | edit source]guideline
[edit | edit source]- 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
[edit | edit source]See Guideline 5.6.9.
rationale
[edit | edit source]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
[edit | edit source]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.
Erroneous execution and bounded errors
[edit | edit source]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, §1.1.5 [Annotated] 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 [Annotated], "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, §1.1.5 [Annotated]. 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.
Unchecked Conversion
[edit | edit source]guideline
[edit | edit source]- Use Ada.Unchecked_Conversion only with the utmost care (Ada Reference Manual 1995, §13.9 [Annotated]).
- 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
[edit | edit source]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
[edit | edit source]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 [Annotated] lists the full rules concerning data validity.
Unchecked Deallocation
[edit | edit source]guideline
[edit | edit source]- 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
[edit | edit source]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.
Unchecked Access
[edit | edit source]guideline
[edit | edit source]- 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
[edit | edit source]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 [Annotated]).
exceptions
[edit | edit source]The Ada Reference Manual 1995, §13.10 [Annotated]) 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."
Address Clauses
[edit | edit source]guideline
[edit | edit source]- 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
[edit | edit source]Single_Address : constant System.Address := System.Storage_Elements.To_Address(...);
Interrupt_Vector_Table : Hardware_Array;
for Interrupt_Vector_Table'Address use Single_Address;
rationale
[edit | edit source]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).
Suppression of Exception Check
[edit | edit source]guideline
[edit | edit source]- 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
[edit | edit source]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.
Initialization
[edit | edit source]guideline
[edit | edit source]- 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
[edit | edit source]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
[edit | edit source]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
[edit | edit source]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
[edit | edit source]- 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
[edit | edit source]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 [Annotated]). 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
[edit | edit source]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.
Exception Propagation
[edit | edit source]guideline
[edit | edit source]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
[edit | edit source]Using Finalize or Adjust to propagate an exception results in a bounded error (Ada Reference Manual 1995, §7.6.1 [Annotated]). Either the exception will be ignored or a Program_Error exception will be raised.
Protected Objects
[edit | edit source]guideline
[edit | edit source]Do not invoke a potentially blocking operation within a protected entry, a protected procedure, or a protected function.
rationale
[edit | edit source]The Ada Reference Manual 1995, §9.5.1 [Annotated] 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.
Abort Statement
[edit | edit source]guideline
[edit | edit source]- 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
[edit | edit source]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 [Annotated] 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.
Summary
[edit | edit source]optional parts of the syntax
[edit | edit source]- 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
[edit | edit source]- 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
[edit | edit source]- 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
[edit | edit source]- 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
[edit | edit source]- 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
[edit | edit source]- 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
[edit | edit source]- 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
[edit | edit source]- 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
[edit | edit source]- Use Ada.Unchecked_Conversion only with the utmost care (Ada Reference Manual 1995, §13.9 [Annotated]).
- 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.