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
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.
- Associate names with loops when they are nested (Booch 1986, 1987).
- Associate names with any loop that contains an
exit Process_All_The_Lines_On_This_Page when Line_Number = Max_Lines_On_Page;
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;
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.
- Associate names with blocks when they are nested.
begin -- Trip
begin -- Arrive_At_Airport
begin -- Visit_Customer
-- again a set of activities...
begin -- Departure_Preparation
When there is a nested block structure, it can be difficult to determine
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.
- Use loop names on all
exitstatements from nested loops.
See the example in 5.1.1 .
exitstatement is an implicit
goto. It should specify its source
explicitly. When there is a nested loop structure and an
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
Naming End Statements
- 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
- Include the designator at the end of a subprogram body.
- Include the defining identifier at the end of a protected unit declaration.
package Autopilot is
function Is_Engaged return Boolean;
package body Autopilot is
task Course_Monitor is
entry Reset (Engage : in Boolean);
function Is_Engaged return Boolean is
procedure Engage is
procedure Disengage is
task body Course_Monitor is
accept Reset (Engage : in Boolean) do
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.
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.
- Name formal parameters descriptively to reduce the need for comments.
List_Manager.Insert (Element => New_Employee,
Into_List => Probationary_Employees,
At_Position => 1);
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.
- 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.
- 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.
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);
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.
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.
A consequence of named parameter association is that the formal parameter names may not be changed without modifying the text of each call.
- 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.
Ada Reference Manual (1995) contains many examples of this practice.
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."
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.
- Show the mode indication of all procedure and entry parameters (Nissen and Wallis 1984 ).
- Use the most restrictive parameter mode applicable to your application.
procedure Open_File (File_Name : in String;
Open_Status : out Status_Codes);
entry Acquire (Key : in Capability;
Resource : out Tape_Drive);
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
Use the mode that reflects the actual use of the parameter. You should
avoid the tendency to make all parameters
in out mode because
mode parameters may be examined as well as updated.
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
in out, potentially causing changes to all places that the routine
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
- 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.
Table is a building block for the creation of new types:
type Table is
Count : List_Size := Empty;
List : Entry_List := Empty_List;
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;
Source_Tail has a value outside the range of
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);
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.
Columns and the subtype
Listing_Paper in the example above
demonstrate how to allow sentinel values. The subtype
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,
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
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
tagged. Similarly, if the abstraction is a member of a family of
abstractions with well-defined variable and common properties, you
should consider a
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.
- 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.
type Buffer_Index is range 1 .. 80;
type Buffer is array (Buffer_Index) of Character;
Input_Line : Buffer;
Input_Line : array (Buffer_Index) of Character;
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 ).
For anonymous task types, see Guideline 6.1.4 .
If you are creating a unique table, for example, the periodic table of the elements, consider using an anonymous array type.
- 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.
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);
type Frame_Header is new Ada.Finalization.Controlled with
-- 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
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
'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.
The predefined packages
Sequential_IOdo 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
- 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.
The following example is taken from the Rationale (1995, §3.7.2) :
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)
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
package Float_Integration is
new Generic_Integration (Float_Type => Float);
begin -- Try_Estimate
Answer := Integrate (F => Residue'Access,
From => Lower,
To => Upper);
Access-to-subprogram types allow you to create data structures that contain subprogram references. There are many uses for this feature, for instance, implementing state machines, call backs in the X Window System, iterators (the operation to be applied to each element of a list), and numerical algorithms (e.g., integration function) (Rationale 1995, §3.7.2 ).
You can achieve the same effect as access-to-subprogram types for dynamic selection by using abstract tagged types. You declare an abstract type with one abstract operation and then use an access-to-class-wide type to get the dispatching effect. This technique provides greater flexibility and type safety than access-to-subprogram types (Ada Reference Manual 1995, §3.10.2 ).
Access-to-subprogram types are useful in implementing dynamic selection. References to the subprograms can be stored directly in the data structure. In a finite state machine, for example, a single data structure can describe the action to be taken on state transitions. Strong type checking is maintained because Ada 95 requires that the designated subprogram has the same parameter/result profile as the one specified in the subprogram access type.
See also Guideline 7.3.2 .
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.
- 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.
An object of type
Name_Holder_1 could potentially hold a string whose
type Number_List is array (Integer range <>) of Integer;
type Number_Holder_1 (Current_Length : Natural := 0) is
Numbers : Number_List (1 .. Current_Length);
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
Numbers : Number_List (1 .. Current_Length);
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
- Use records to group heterogeneous but related data.
- Consider records to map to I/O device data.
type Propulsion_Method is (Sail, Diesel, Nuclear);
type Craft is
Name : Common_Name;
Plant : Propulsion_Method;
Length : Feet;
Beam : Feet;
Draft : Feet;
type Fleet is array (1 .. Fleet_Size) of Craft;
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.
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.
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
- 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).
An array of type
Employee_List can contain pointers to part-time and
full-time employees (and possibly other kinds of employees in the
package Personnel is
type Employee is tagged limited private;
type Reference is access all Employee'Class;
package Part_Time_Staff is
type Part_Time_Employee is new Personnel.Employee with
package Full_Time_Staff is
type Full_Time_Employee is new Personnel.Employee with
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;
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
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.
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.
- 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.
type Coordinate is
Row : Local_Float;
Column : Local_Float;
type Window is
Top_Left : Coordinate;
Bottom_Right : Coordinate;
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 .
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 . . .
- 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
- 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
These lines show how a dangling reference might be created:
P1 := new Object;
P2 := P1;
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:
package List is
type Object is private;
function "=" (Left, Right : Object) return Boolean; -- element-by-element comparison
... -- Operations go here
type Handle is access List.Object;
type Object is new Ada.Finalization.Controlled with
Next : List.Handle;
... -- Useful information go here
procedure Adjust (L : in out List.Object);
procedure Finalize (L : in out List.Object);
package body List is
Free_List : List.Handle;
procedure Adjust (L : in out List.Object) is
L := Deep_Copy (L);
procedure Finalize (L : in out List.Object) is
-- Chain L to Free_List
See also 6.3.2 for variations on these problems. A dynamically allocated
object is an object created by the execution of an allocator (
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
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
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
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
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
compare the contents, not the reference. You should implement your
structure as a controlled type. If you want copy semantics, you can
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
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
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.
If a composite object is large enough to be allocated on the heap, you
can still declare it as an
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
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.
- 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.
package Message_Services is
type Message_Code_Type is range 0 .. 100;
subtype Message is String;
function Get_Message (Message_Code: Message_Code_Type)
pragma Inline (Get_Message);
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,
function Get_Message (Message_Code : Message_Code_Type)
return Message is
return Message_Table (Message_Code).all;
The following code fragment shows a use of aliased objects, using the
'Access to implement a generic component that manages hashed
collections of objects:
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;
type Access_Cell is access Cell;
package body Collection is
type Cell is
Value : aliased Collection.Object;
Link : Access_Cell;
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);
while Current /= null loop
if Current.Value = Object then
Current := Current.Link;
-- The exported one
function Find (Object : in Collection.Object) return Collection.Handle is
Index : constant Hash_Index := Hash (Object);
return Find (Object, Index);
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.
- Use access discriminants to create self-referential data structures, i.e., a data structure one of whose components points to the enclosing structure.
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).
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.
- Use modular types rather than Boolean arrays when you create data
structures that need bit-wise operations, such as
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);
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 .
Properly coded expressions can enhance the readability and understandability of a program. Poorly coded expressions can turn a program into a maintainer's nightmare.
'Lastinstead of numeric literals to represent the first or last values of a range.
'Rangeor the subtype name of the range instead of
'First .. 'Last.
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;
In the example above, it is better to use
Weather_Stations in the
for loop than to use
.. 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
in the offset calculation than to use
All_Time_Low because the code
will still be correct if the definition of the subtype
changed. This enhances program reliability.
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;
- Use array attributes
'Lengthinstead of numeric literals for accessing arrays.
- Use the
'Rangeof the array instead of the name of the index subtype to express a range.
'First .. 'Lastto express a range.
subtype Name_String is String (1 .. Name_Length);
File_Path : Name_String := (others => ' ');
for I in File_Path'Range loop
In the example above, it is better to use
Name_String'Range in the
for loop than to use
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 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.
- 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.
(1.5 * X**2)/A - (6.5*X + 47.0)
2*I + 4*Y + 8*Z + C
The Ada rules of operator precedence are defined in the Ada Reference Manual (1995, §4.5) and follow the same commonly accepted precedence of algebraic operators. The strong typing facility in Ada combined with the common precedence rules make many parentheses unnecessary. However, when an uncommon combination of operators occurs, it may be helpful to add parentheses even when the precedence rules apply. The expression:
5 + ((Y ** 3) mod 10)
is clearer, and equivalent to:
5 + Y**3 mod 10
The rules of evaluation do specify left to right evaluation for operators with the same precedence level. However, it is the most commonly overlooked rule of evaluation when checking expressions for correctness.
Positive Forms of Logic
- 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.
if Operator_Missing then
rather than either:
if not Operator_Found then
if not Operator_Missing then
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.
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
- 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.
if Y /= 0 or else (X/Y) /= 10 then
if Y /= 0 then
if (X/Y) /= 10 then
rather than either:
if Y /= 0 and (X/Y) /= 10 then
if (X/Y) /= 10 then
if Target /= null and then Target.Distance < Threshold then
if Target.Distance < Threshold then
to avoid referencing a field in a nonexistent object.
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
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
>=in relational expressions with real operands instead of
Current_Temperature : Temperature := 0.0;
Temperature_Increment : Temperature := 1.0 / 3.0;
Maximum_Temperature : constant := 100.0;
Current_Temperature + Temperature_Increment;
exit when Current_Temperature >= Maximum_Temperature;
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
more portable than either
Floating-point arithmetic is treated in Guideline 7.2.7 .
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 .
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.
- 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 ).
- Do not nest expressions or control structures beyond a nesting level of five.
The following section of code:
if not Condition_1 then
if Condition_2 then
else -- not Condition_2
else -- Condition_1
can be rewritten more clearly and with less nesting as:
if Condition_1 then
elsif Condition_2 then
else -- not (Condition_1 or Condition_2)
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
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.
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
ifstatements into a
- Am I using
else ifwhere I could be using
- Can I reorder the conditional expressions controlling this nested structure?
- Is there a different design that would be simpler?
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.
- Use slices rather than a loop to copy part of an array.
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
Markov_Probabilities(I)(Index'Pred(I) .. Index'Succ(I));
Diagonal_Data(Index'Last)(First .. Second) :=
Markov_Probabilities(Index'Last)(Index'Pred(Index'Last) .. Index'Last);
Diagonal_Data(Index'Last)(Third) := Null_Value;
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.
- Minimize the use of an
otherschoice in a
- Do not use ranges of enumeration literals in
casestatements rather than
if/elsifstatements, wherever possible.
- Use type extension and dispatching rather than
casestatements if, possible.
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
statement. If the choices had been enumerated explicitly, as
when Red | Green | Blue => instead of
Red .. Blue =>, then the
statement would not have compiled. This would have forced the maintainer
to make a conscious decision about what to do in the case of
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
handles errors in user input. The less readable alternative with the
if/elsif statement is shown after the
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
end loop Do_Menu_Choices_1;
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
end loop Do_Menu_Choices_2;
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
when the subtype of the case expression has many values, for example,
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
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
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 .
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 ..
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;
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'.
This page of the "Ada Quality and Style Guide" has been adapted from the original work at https://en.wikibooks.org/wiki/Ada_Style_Guide, which is licensed under the Creative Commons Attribution-ShareAlike License; additional terms may apply. Page not endorsed by Wikibooks or the Ada Style Guide Wikibook authors. This page is licensed under the same license as the original work.