Programming Practices
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.
Loop Names
guideline
- Associate names with loops when they are nested (Booch 1986, 1987).
- Associate names with any loop that contains an
exit
statement.
example
Process_Each_Page:
loop
Process_All_The_Lines_On_This_Page:
loop
...
exit Process_All_The_Lines_On_This_Page when Line_Number = Max_Lines_On_Page;
...
Look_For_Sentinel_Value:
loop
...
exit Look_For_Sentinel_Value when Current_Symbol = Sentinel;
...
end loop Look_For_Sentinel_Value;
...
end loop Process_All_The_Lines_On_This_Page;
...
exit Process_Each_Page when Page_Number = Maximum_Pages;
...
end loop Process_Each_Page;
rationale
When you associate a name with a loop, you must include that name with the associated end for that loop (Ada Reference Manual 1995). This helps readers find the associated end for any given loop. This is especially true if loops are broken over screen or page boundaries. The choice of a good name for the loop documents its purpose, reducing the need for explanatory comments. If a name for a loop is very difficult to choose, this could indicate a need for more thought about the algorithm.
Regularly naming loops helps you follow Guideline 5.1.3. Even in the
face of code changes, for example, adding an outer or inner loop, the
exit
statement does not become ambiguous.
It can be difficult to think up a name for every loop; therefore, the guideline specifies nested loops. The benefits in readability and second thought outweigh the inconvenience of naming the loops.
Block Names
guideline
- Associate names with blocks when they are nested.
example
Trip:
declare
...
begin -- Trip
Arrive_At_Airport:
declare
...
begin -- Arrive_At_Airport
Rent_Car;
Claim_Baggage;
Reserve_Hotel;
...
end Arrive_At_Airport;
Visit_Customer:
declare
...
begin -- Visit_Customer
-- again a set of activities...
...
end Visit_Customer;
Departure_Preparation:
declare
...
begin -- Departure_Preparation
Return_Car;
Check_Baggage;
Wait_For_Flight;
...
end Departure_Preparation;
Board_Return_Flight;
end Trip;
rationale
When there is a nested block structure, it can be difficult to determine
which end
corresponds to which block. Naming blocks alleviates this
confusion. The choice of a good name for the block documents its
purpose, reducing the need for explanatory comments. If a name for the
block is very difficult to choose, this could indicate a need for more
thought about the algorithm.
This guideline is also useful if nested blocks are broken over a screen or page boundary.
It can be difficult to think up a name for each block; therefore, the guideline specifies nested blocks. The benefits in readability and second thought outweigh the inconvenience of naming the blocks.
Exit Statements
guideline
- Use loop names on all
exit
statements from nested loops.
example
See the example in 5.1.1 .
rationale
An exit
statement is an implicit goto
. It should specify its source
explicitly. When there is a nested loop structure and an exit
statement
is used, it can be difficult to determine which loop is being exited.
Also, future changes that may introduce a nested loop are likely to
introduce an error, with the exit
accidentally exiting from the wrong
loop. Naming loops and their exits alleviates this confusion. This
guideline is also useful if nested loops are broken over a screen or
page boundary.
Naming End Statements
guideline
- 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.
example
------------------------------------------------------------------------
package Autopilot is
function Is_Engaged return Boolean;
procedure Engage;
procedure Disengage;
end Autopilot;
------------------------------------------------------------------------
package body Autopilot is
...
---------------------------------------------------------------------
task Course_Monitor is
entry Reset (Engage : in Boolean);
end Course_Monitor;
---------------------------------------------------------------------
function Is_Engaged return Boolean is
...
end Is_Engaged;
---------------------------------------------------------------------
procedure Engage is
...
end Engage;
---------------------------------------------------------------------
procedure Disengage is
...
end Disengage;
---------------------------------------------------------------------
task body Course_Monitor is
...
accept Reset (Engage : in Boolean) do
...
end Reset;
...
end Course_Monitor;
---------------------------------------------------------------------
end Autopilot;
------------------------------------------------------------------------
rationale
Repeating names on the end of these compound statements ensures
consistency throughout the code. In addition, the named end
provides a
reference for the reader if the unit spans a page or screen boundary or
if it contains a nested unit.
Parameter Lists
A subprogram or entry parameter list is the interface to the abstraction implemented by the subprogram or entry. It is important that it is clear and that it is expressed in a consistent style. Careful decisions about formal parameter naming and ordering can make the purpose of the subprogram easier to understand, which can make it easier to use.
Formal Parameters
guideline
- Name formal parameters descriptively to reduce the need for comments.
example
List_Manager.Insert (Element => New_Employee,
Into_List => Probationary_Employees,
At_Position => 1);
rationale
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
guideline
- 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
- 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
Encode_Telemetry_Packet (Source => Power_Electronics,
Content => Temperature,
Value => Read_Temperature_Sensor(Power_Electronics),
Time => Current_Time,
Sequence => Next_Packet_ID,
Vehicle => This_Spacecraft,
Primary_Module => True);
rationale
Calls of infrequently used subprograms or entries with many formal parameters can be difficult to understand without referring to the subprogram or entry code. Named parameter association can make these calls more readable.
When the formal parameters have been named appropriately, it is easier to determine exactly what purpose the subprogram serves without looking at its code. This reduces the need for named constants that exist solely to make calls more readable. It also allows variables used as actual parameters to be given names indicating what they are without regard to why they are being passed in a call. An actual parameter, which is an expression rather than a variable, cannot be named otherwise.
Named association allows subprograms to have new parameters inserted with minimal ramifications to existing calls.
notes
The judgment of when named parameter association improves readability is subjective. Certainly, simple or familiar subprograms, such as a swap routine or a sine function, do not require the extra clarification of named association in the procedure call.
caution
A consequence of named parameter association is that the formal parameter names may not be changed without modifying the text of each call.
Default Parameters
guideline
- 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
Ada Reference Manual (1995) contains many examples of this practice.
rationale
Often, the majority of uses of a subprogram or entry need the same value for a given parameter. Providing that value, as the default for the parameter, makes the parameter optional on the majority of calls. It also allows the remaining calls to customize the subprogram or entry by providing different values for that parameter.
Placing default parameters at the end of the formal parameter list allows the caller to use positional association on the call; otherwise, defaults are available only when named association is used.
Often during maintenance activities, you increase the functionality of a subprogram or entry. This requires more parameters than the original form for some calls. New parameters may be required to control this new functionality. Give the new parameters default values that specify the old functionality. Calls needing the old functionality need not be changed; they take the defaults. This is true if the new parameters are added to the end of the parameter list, or if named association is used on all calls. New calls needing the new functionality can specify that by providing other values for the new parameters.
This enhances maintainability in that the places that use the modified routines do not themselves have to be modified, while the previous functionality levels of the routines are allowed to be "reused."
exceptions
Do not go overboard. If the changes in functionality are truly radical, you should be preparing a separate routine rather than modifying an existing one. One indicator of this situation would be that it is difficult to determine value combinations for the defaults that uniquely and naturally require the more restrictive of the two functions. In such cases, it is better to go ahead with creation of a separate routine.
Mode Indication
guideline
- 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
procedure Open_File (File_Name : in String;
Open_Status : out Status_Codes);
entry Acquire (Key : in Capability;
Resource : out Tape_Drive);
rationale
By showing the mode of parameters, you aid the reader. If you do not
specify a parameter mode, the default mode is in
. Explicitly showing
the mode indication of all parameters is a more assertive action than
simply taking the default mode. Anyone reviewing the code later will be
more confident that you intended the parameter mode to be in
.
Use the mode that reflects the actual use of the parameter. You should
avoid the tendency to make all parameters in out
mode because out
mode parameters may be examined as well as updated.
exceptions
It may be necessary to consider several alternative implementations for
a given abstraction. For example, a bounded stack can be implemented as
a pointer to an array. Even though an update to the object being pointed
to does not require changing the pointer value itself, you may want to
consider making the mode in out
to allow changes to the implementation
and to document more accurately what the operation is doing. If you
later change the implementation to a simple array, the mode will have to
be in out
, potentially causing changes to all places that the routine
is called.
Types
In addition to determining the possible values for variables and subtype names, type distinctions can be very valuable aids in developing safe, readable, and understandable code. Types clarify the structure of your data and can limit or restrict the operations performed on that data.
"Keeping types distinct has been found to be a very powerful means of detecting logical mistakes when a program is written and to give valuable assistance whenever the program is being subsequently maintained" (Pyle 1985 ). Take advantage of Ada's strong typing capability in the form of subtypes, derived types, task types, protected types, private types, and limited private types.The guidelines encourage much code to be written to ensure strong typing. While it might appear that there would be execution penalties for this amount of code, this is usually not the case. In contrast to other conventional languages, Ada has a less direct relationship between the amount of code that is written and the size of the resulting executable program. Most of the strong type checking is performed at compilation time rather than execution time, so the size of the executable code is not greatly affected.
For guidelines on specific kinds of data structures and tagged types, see 9.2.1 , respectively.
Derived Types and Subtypes
guideline
- 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
Type Table
is a building block for the creation of new types:
type Table is
record
Count : List_Size := Empty;
List : Entry_List := Empty_List;
end record;
type Telephone_Directory is new Table;
type Department_Inventory is new Table;
The following are distinct types that cannot be intermixed in operations that are not programmed explicitly to use them both:
type Dollars is new Number;
type Cents is new Number;
Below, Source_Tail
has a value outside the range of Listing_Paper
when the line is empty. All the indices can be mixed in expressions, as
long as the results fall within the correct subtypes:
type Columns is range First_Column - 1 .. Listing_Width + 1;
subtype Listing_Paper is Columns range First_Column .. Listing_Width;
subtype Dumb_Terminal is Columns range First_Column .. Dumb_Terminal_Width;
type Line is array (Columns range <>) of Bytes;
subtype Listing_Line is Line (Listing_Paper);
subtype Terminal_Line is Line (Dumb_Terminal);
Source_Tail : Columns := Columns'First;
Source : Listing_Line;
Destination : Terminal_Line;
...
Destination(Destination'First .. Source_Tail - Destination'Last) :=
Source(Columns'Succ(Destination'Last) .. Source_Tail);
rationale
The name of a derived type can make clear its intended use and avoid proliferation of similar type definitions. Objects of two derived types, even though derived from the same type, cannot be mixed in operations unless such operations are supplied explicitly or one is converted to the other explicitly. This prohibition is an enforcement of strong typing.
Define new types, derived types, and subtypes cautiously and deliberately. The concepts of subtype and derived type are not equivalent, but they can be used to advantage in concert. A subtype limits the range of possible values for a type but does not define a new type.
Types can have highly constrained sets of values without eliminating useful values. Used in concert, derived types and subtypes can eliminate many flag variables and type conversions within executable statements. This renders the program more readable, enforces the abstraction, and allows the compiler to enforce strong typing constraints.
Many algorithms begin or end with values just outside the normal range. If boundary values are not compatible within subexpressions, algorithms can be needlessly complicated. The program can become cluttered with flag variables and special cases when it could just test for zero or some other sentinel value just outside normal range.
The type Columns
and the subtype Listing_Paper
in the example above
demonstrate how to allow sentinel values. The subtype Listing_Paper
could be used as the type for parameters of subprograms declared in the
specification of a package. This would restrict the range of values that
could be specified by the caller. Meanwhile, the type Columns
could be
used to store such values internally to the body of the package,
allowing First_Column - 1
to be used as a sentinel value. This
combination of types and subtypes allows compatibility between subtypes
within subexpressions without type conversions as would happen with
derived types.
The choice between type derivation and type extension depends on what kind of changes you expect to occur to objects in the type. In general, type derivation is a very simple form of inheritance: the derived types inherit the structure, operations, and values of the parent type (Rationale 1995, §4.2 ). Although you can add operations, you cannot augment the data structure. You can derive from either scalar or composite types.
Type extension is a more powerful form of inheritance, only applied to
tagged
records, in which you can augment both the type's components
and operations. When the record implements an abstraction with the
potential for reuse and/or extension, it is a good candidate for making
it tagged
. Similarly, if the abstraction is a member of a family of
abstractions with well-defined variable and common properties, you
should consider a tagged
record.
notes
The price of the reduction in the number of independent type declarations is that subtypes and derived types change when the base type is redefined. This trickle-down of changes is sometimes a blessing and sometimes a curse. However, usually it is intended and beneficial.
Anonymous Types
guideline
- 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
Use:
type Buffer_Index is range 1 .. 80;
type Buffer is array (Buffer_Index) of Character;
Input_Line : Buffer;
rather than:
Input_Line : array (Buffer_Index) of Character;
rationale
Although Ada allows anonymous types, they have limited usefulness and complicate program modification. For example, except for arrays, a variable of anonymous type can never be used as an actual parameter because it is not possible to define a formal parameter of the same type. Even though this may not be a limitation initially, it precludes a modification in which a piece of code is changed to a subprogram. Although you can declare the anonymous array to be aliased, you cannot use this access value as an actual parameter in a subprogram because the subprogram's formal parameter declaration requires a type mark. Also, two variables declared using the same anonymous type declaration are actually of different types.
Even though the implicit conversion of array types during parameter passing is supported in Ada, it is difficult to justify not using the type of the parameter. In most situations, the type of the parameter is visible and easily substituted in place of an anonymous array type. The use of an anonymous array type implies that the array is only being used as a convenient way to implement a collection of values. It is misleading to use an anonymous type, and then treat the variable as an object.
When you use an access parameter or access discriminant, the anonymous type is essentially declared inside the subprogram or object itself (Rationale 1995, §3.7.1 ). Thus, you have no way of declaring another object of the same type, and the object is treated as a constant. In the case of a self-referential data structure (see
Guideline 5.4.6 ), you need the access parameter to be able to
manipulate the data the discriminant accesses (Rationale 1995, §3.7.1 ).
notes
For anonymous task types, see Guideline 6.1.4 .
exceptions
If you are creating a unique table, for example, the periodic table of the elements, consider using an anonymous array type.
Private Types
guideline
- 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
------------------------------------------------------------------------
with Ada.Finalization;
package Packet_Telemetry is
type Frame_Header is new Ada.Finalization.Controlled with private;
type Frame_Data is private;
type Frame_Codes is (Main_Bus_Voltage, Transmitter_1_Power);
...
private
type Frame_Header is new Ada.Finalization.Controlled with
record
...
end record;
-- override adjustment and finalization to get correct assignment semantics
procedure Adjust (Object : in out Frame_Header);
procedure Finalize (Object : in out Frame_Header);
type Frame_Data is
record
...
end record;
...
end Packet_Telemetry;
------------------------------------------------------------------------
rationale
Limited private types and private types support abstraction and information hiding better than nonprivate types. The more restricted the type, the better information hiding is served. This, in turn, allows the implementation to change without affecting the rest of the program. While there are many valid reasons to export types, it is better to try the preferred route first, loosening the restrictions only as necessary. If it is necessary for a user of the package to use a few of the restricted operations, it is better to export the operations explicitly and individually via exported subprograms than to drop a level of restriction. This practice retains the restrictions on other operations.
Limited private types have the most restricted set of operations
available to users of a package. Of the types that must be made
available to users of a package, as many as possible should be derived
from the controlled types or limited private. Controlled types give you
the ability to adjust assignment and to finalize values, so you no
longer need to create limited private types to guarantee a client that
assignment and equality obey deep copy/comparison semantics. Therefore,
it is possible to export a slightly less restrictive type (i.e., private
type that extends Ada.Finalization.Controlled
) that has an adjustable
assignment operator and overridable equality operator. See also
Guideline 5.4.5 .
The operations available to limited private types are membership tests,
selected components, components for the selections of any discriminant,
qualification and explicit conversion, and attributes 'Base
and
'Size
. Objects of a limited private type also have the attribute
'Constrained
if there are discriminants. None of these operations
allows the user of the package to manipulate objects in a way that
depends on the structure of the type.
notes
The predefined packages Direct_IO
and Sequential_IO
do not accept
limited private types as generic parameters. This restriction should be
considered when I/O operations are needed for a type.
See Guideline 8.3.3 for a discussion of the use of private and limited private types in generic units.
Subprogram Access Types
guideline
- 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
The following example is taken from the Rationale (1995, §3.7.2) :
generic
type Float_Type is digits <>;
package Generic_Integration is
type Integrand is access function (X : Float_Type) return Float_Type;
function Integrate (F : Integrand;
From : Float_Type;
To : Float_Type;
Accuracy : Float_Type := 10.0*Float_Type'Model_Epsilon)
return Float_Type;
end Generic_Integration;
with Generic_Integration;
procedure Try_Estimate (External_Data : in Data_Type;
Lower : in Float;
Upper : in Float;
Answer : out Float) is
-- external data set by other means
function Residue (X : Float) return Float is
Result : Float;
begin -- Residue
-- compute function value dependent upon external data
return Result;
end Residue;
package Float_Integration is
new Generic_Integration (Float_Type => Float);
use Float_Integration;
begin -- Try_Estimate
...
Answer := Integrate (F => Residue'Access,
From => Lower,
To => Upper);
end Try_Estimate;
rationale
Access-to-subprogram types allow you to create data structures that contain subprogram references. There are many uses for this feature, for instance, implementing state machines, call backs in the X Window System, iterators (the operation to be applied to each element of a list), and numerical algorithms (e.g., integration function) (Rationale 1995, §3.7.2 ).
You can achieve the same effect as access-to-subprogram types for dynamic selection by using abstract tagged types. You declare an abstract type with one abstract operation and then use an access-to-class-wide type to get the dispatching effect. This technique provides greater flexibility and type safety than access-to-subprogram types (Ada Reference Manual 1995, §3.10.2 ).
Access-to-subprogram types are useful in implementing dynamic selection. References to the subprograms can be stored directly in the data structure. In a finite state machine, for example, a single data structure can describe the action to be taken on state transitions. Strong type checking is maintained because Ada 95 requires that the designated subprogram has the same parameter/result profile as the one specified in the subprogram access type.
See also Guideline 7.3.2 .
Data Structures
The data structuring capabilities of Ada are a powerful resource; therefore, use them to model the data as closely as possible. It is possible to group logically related data and let the language control the abstraction and operations on the data rather than requiring the programmer or maintainer to do so. Data can also be organized in a building block fashion. In addition to showing how a data structure is organized (and possibly giving the reader an indication as to why it was organized that way), creating the data structure from smaller components allows those components to be reused. Using the features that Ada provides can increase the maintainability of your code.
Discriminated Records
guideline
- 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
An object of type Name_Holder_1
could potentially hold a string whose
length is Natural'Last
:
type Number_List is array (Integer range <>) of Integer;
type Number_Holder_1 (Current_Length : Natural := 0) is
record
Numbers : Number_List (1 .. Current_Length);
end record;
An object of type Name_Holder_2
imposes a more reasonable restriction
on the length of its string component:
type Number_List is array (Integer range <>) of Integer;
subtype Max_Numbers is Natural range 0 .. 42;
type Number_Holder_2 (Current_Length : Max_Numbers := 0) is
record
Numbers : Number_List (1 .. Current_Length);
end record;
rationale
When you use the discriminant to constrain an array inside a discriminated record, the larger the range of values the discriminant can assume, the more space an object of the type might require. Although your program may compile and link, it will fail at execution when the run-time system is unable to create an object of the potential size required.
The discriminated record captures the intent of an array whose bounds
may vary at run-time. A simple constrained array definition (e.g., type Number_List is array (1 .. 42) of Integer;
) does not capture the intent
that there are at most 42 possible numbers in the list.
Heterogeneous Related Data
guideline
- Use records to group heterogeneous but related data.
- Consider records to map to I/O device data.
example
type Propulsion_Method is (Sail, Diesel, Nuclear);
type Craft is
record
Name : Common_Name;
Plant : Propulsion_Method;
Length : Feet;
Beam : Feet;
Draft : Feet;
end record;
type Fleet is array (1 .. Fleet_Size) of Craft;
rationale
You help the maintainer find all of the related data by gathering it into the same construct, simplifying any modifications that apply to all rather than part. This, in turn, increases reliability. Neither you nor an unknown maintainer is liable to forget to deal with all the pieces of information in the executable statements, especially if updates are done with aggregate assignments whenever possible.
The idea is to put the information a maintainer needs to know where it
can be found with the minimum of effort. For example, if all information
relating to a given Craft
is in the same place, the relationship is
clear both in the declarations and especially in the code accessing and
updating that information. But, if it is scattered among several data
structures, it is less obvious that this is an intended relationship as
opposed to a coincidental one. In the latter case, the declarations may
be grouped together to imply intent, but it may not be possible to group
the accessing and updating code that way. Ensuring the use of the same
index to access the corresponding element in each of several parallel
arrays is difficult if the accesses are at all scattered.
If the application must interface directly to hardware, the use of records, especially in conjunction with record representation clauses, could be useful to map onto the layout of the hardware in question.
notes
It may seem desirable to store heterogeneous data in parallel arrays in what amounts to a FORTRAN-like style. This style is an artifact of FORTRAN's data structuring limitations. FORTRAN only has facilities for constructing homogeneous arrays.
exceptions
If the application must interface directly to hardware, and the hardware requires that information be distributed among various locations, then it may not be possible to use records.
Heterogeneous Polymorphic Data
guideline
- 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
An array of type Employee_List
can contain pointers to part-time and
full-time employees (and possibly other kinds of employees in the
future):
-----------------------------------------------------------------------------------
package Personnel is
type Employee is tagged limited private;
type Reference is access all Employee'Class;
...
private
...
end Personnel;
-----------------------------------------------------------------------------------
with Personnel;
package Part_Time_Staff is
type Part_Time_Employee is new Personnel.Employee with
record
...
end record;
...
end Part_Time_Staff;
-----------------------------------------------------------------------------------
with Personnel;
package Full_Time_Staff is
type Full_Time_Employee is new Personnel.Employee with
record
...
end record;
...
end Full_Time_Staff;
-----------------------------------------------------------------------------------
...
type Employee_List is array (Positive range <>) of Personnel.Reference;
Current_Employees : Employee_List (1..10);
...
Current_Employees(1) := new Full_Time_Staff.Full_Time_Employee;
Current_Employees(2) := new Part_Time_Staff.Part_Time_Employee;
...
rationale
Polymorphism is a means of factoring out the differences among a
collection of abstractions so that programs may be written in terms of
the common properties. Polymorphism allows the different objects in a
heterogeneous data structure to be treated the same way, based on
dispatching operations defined on the root tagged type. This eliminates
the need for case
statements to select the processing required for
each specific type. Guideline 5.6.3 discusses the maintenance impact of
using case
statements.
Enumeration types, variant records, and case statements are hard to maintain because the expertise on a given variant of the data type tends to be spread all over the program. When you create a tagged type hierarchy (tagged types and type extension), you can avoid the variant records, case statement, and single enumeration type that only supports the variant record discriminant. Moreover, you localize the "expertise" about the variant within the data structure by having all the corresponding primitives for a single operation call common "operation-specific" code.
See also Guideline 9.2.1 for a more detailed discussion of tagged types.
exceptions
In some instances, you may want to use a variant record approach to organize modularity around operations. For graphic output, for example, you may find it more maintainable to use variant records. You must make the tradeoff of whether adding a new operation will be less work than adding a new variant.
Nested Records
guideline
- 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
type Coordinate is
record
Row : Local_Float;
Column : Local_Float;
end record;
type Window is
record
Top_Left : Coordinate;
Bottom_Right : Coordinate;
end record;
rationale
You can make complex data structures understandable and comprehensible by composing them of familiar building blocks. This technique works especially well for large record types with parts that fall into natural groupings. The components factored into separately declared records, based on a common quality or purpose, correspond to a lower level of abstraction than that represented by the larger record.
When designing a complex data structure, you must consider whether type composition or type extension is the best suited technique. Type composition refers to creating a record component whose type is itself a record. You will often need a hybrid of these techniques, that is, some components you include through type composition and others you create through type extension. Type extension may provide a cleaner design if the "intermediate" records are all instances of the same abstraction family. See also Guidelines 5.4.2 and 9.2.1 .
notes
A carefully chosen name for the component of the larger record that is used to select the smaller enhances readability, for example:
if Window1.Bottom_Right.Row > Window2.Top_Left.Row then . . .
Dynamic Data
guideline
- 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
These lines show how a dangling reference might be created:
P1 := new Object;
P2 := P1;
Unchecked_Object_Deallocation(P2);
This line can raise an exception due to referencing the deallocated object:
X := P1.all;
In the following three lines, if there is no intervening assignment of
the value of P1
to any other pointer, the object created on the first
line is no longer accessible after the third line. The only pointer to
the allocated object has been dropped:
P1 := new Object;
...
P1 := P2;
The following code shows an example of using Finalize
to make sure
that when an object is finalized (i.e., goes out of scope), the
dynamically allocated elements are chained on a free list:
with Ada.Finalization;
package List is
type Object is private;
function "=" (Left, Right : Object) return Boolean; -- element-by-element comparison
... -- Operations go here
private
type Handle is access List.Object;
type Object is new Ada.Finalization.Controlled with
record
Next : List.Handle;
... -- Useful information go here
end record;
procedure Adjust (L : in out List.Object);
procedure Finalize (L : in out List.Object);
end List;
package body List is
Free_List : List.Handle;
...
procedure Adjust (L : in out List.Object) is
begin
L := Deep_Copy (L);
end Adjust;
procedure Finalize (L : in out List.Object) is
begin
-- Chain L to Free_List
end Finalize;
end List;
rationale
See also 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
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
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
guideline
- 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
package Message_Services is
type Message_Code_Type is range 0 .. 100;
subtype Message is String;
function Get_Message (Message_Code: Message_Code_Type)
return Message;
pragma Inline (Get_Message);
end Message_Services;
package body Message_Services is
type Message_Handle is access constant Message;
Message_0 : aliased constant Message := "OK";
Message_1 : aliased constant Message := "Up";
Message_2 : aliased constant Message := "Shutdown";
Message_3 : aliased constant Message := "Shutup";
. . .
type Message_Table_Type is array (Message_Code_Type) of Message_Handle;
Message_Table : Message_Table_Type :=
(0 => Message_0'Access,
1 => Message_1'Access,
2 => Message_2'Access,
3 => Message_3'Access,
-- etc.
);
function Get_Message (Message_Code : Message_Code_Type)
return Message is
begin
return Message_Table (Message_Code).all;
end Get_Message;
end Message_Services;
The following code fragment shows a use of aliased objects, using the
attribute 'Access
to implement a generic component that manages hashed
collections of objects:
generic
type Hash_Index is mod <>;
type Object is tagged private;
type Handle is access all Object;
with function Hash (The_Object : in Object) return Hash_Index;
package Collection is
function Insert (Object : in Collection.Object) return Collection.Handle;
function Find (Object : in Collection.Object) return Collection.Handle;
Object_Not_Found : exception;
...
private
type Cell;
type Access_Cell is access Cell;
end Collection;
package body Collection is
type Cell is
record
Value : aliased Collection.Object;
Link : Access_Cell;
end record;
type Table_Type is array (Hash_Index) of Access_Cell;
Table : Table_Type;
-- Go through the collision chain and return an access to the useful data.
function Find (Object : in Collection.Object;
Index : in Hash_Index) return Handle is
Current : Access_Cell := Table (Index);
begin
while Current /= null loop
if Current.Value = Object then
return Current.Value'Access;
else
Current := Current.Link;
end if;
end loop;
raise Object_Not_Found;
end Find;
-- The exported one
function Find (Object : in Collection.Object) return Collection.Handle is
Index : constant Hash_Index := Hash (Object);
begin
return Find (Object, Index);
end Find;
...
end Collection;
rationale
Aliasing allows the programmer to have indirect access to declared objects. Because you can update aliased objects through more than one path, you must exercise caution to avoid unintended updates. When you restrict the aliased objects to being constant, you avoid having the object unintentionally modified. In the example above, the individual message objects are aliased constant message strings so their values cannot be changed. The ragged array is then initialized with references to each of these constant strings.
Aliasing allows you to manipulate objects using indirection while avoiding dynamic allocation. For example, you can insert an object onto a linked list without dynamically allocating the space for that object (Rationale 1995, §3.7.1 ).
Another use of aliasing is in a linked data structure in which you try to hide the enclosing container. This is essentially the inverse of a self-referential data structure (see Guideline 5.4.7 ). If a package manages some data using a linked data structure, you may only want to export access values that denote the "useful" data. You can use an access-to-object to return an access to the useful data, excluding the pointers used to chain objects.
Access Discriminants
guideline
- Use access discriminants to create self-referential data structures, i.e., a data structure one of whose components points to the enclosing structure.
example
See the examples in Guidelines 8.3.6 (using access discriminants to build an iterator) and 9.5.1 (using access discriminants in multiple inheritance).
rationale
The access discriminant is essentially a pointer of an anonymous type being used as a discriminant. Because the access discriminant is of an anonymous access type, you cannot declare other objects of the type. Thus, once you initialize the discriminant, you create a "permanent" (for the lifetime of the object) association between the discriminant and the object it accesses. When you create a self-referential structure, that is, a component of the structure is initialized to point to the enclosing object, the "constant" behavior of the access discriminant provides the right behavior to help you maintain the integrity of the structure.
See also Rationale (1995, §4.6.3) for a discussion of access discriminants to achieve multiple views of an object.
See also Guideline 6.1.3 for an example of an access discriminant for a task type.
Modular Types
guideline
- Use modular types rather than Boolean arrays when you create data
structures that need bit-wise operations, such as
and
andor
.
example
with Interfaces;
procedure Main is
type Unsigned_Byte is mod 255;
X : Unsigned_Byte;
Y : Unsigned_Byte;
Z : Unsigned_Byte;
X1 : Interfaces.Unsigned_16;
begin -- Main
Z := X or Y; -- does not cause overflow
-- Show example of left shift
X1 := 16#FFFF#;
for Counter in 1 .. 16 loop
X1 := Interfaces.Shift_Left (Value => X1, Amount => 1);
end loop;
end Main;
rationale
Modular types are preferred when the number of bits is known to be fewer than the number of bits in a word and/or performance is a serious concern. Boolean arrays are appropriate when the number of bits is not particularly known in advance and performance is not a serious issue. See also Guideline 10.6.3 .
Expressions
Properly coded expressions can enhance the readability and understandability of a program. Poorly coded expressions can turn a program into a maintainer's nightmare.
Range Values
guideline
- 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
type Temperature is range All_Time_Low .. All_Time_High;
type Weather_Stations is range 1 .. Max_Stations;
Current_Temperature : Temperature := 60;
Offset : Temperature;
...
for I in Weather_Stations loop
Offset := Current_Temperature - Temperature'First;
...
end loop;
rationale
In the example above, it is better to use Weather_Stations
in the
for
loop than to use Weather_Stations'First
.. Weather_Stations'Last
or 1 .. Max_Stations
because it is clearer,
less error-prone, and less dependent on the definition of the type
Weather_Stations
. Similarly, it is better to use Temperature'First
in the offset calculation than to use All_Time_Low
because the code
will still be correct if the definition of the subtype Temperature
is
changed. This enhances program reliability.
caution
When you implicitly specify ranges and attributes like this, be careful that you use the correct subtype name. It is easy to refer to a very large range without realizing it. For example, given the declarations:
type Large_Range is new Integer;
subtype Small_Range is Large_Range range 1 .. 10;
type Large_Array is array (Large_Range) of Integer;
type Small_Array is array (Small_Range) of Integer;
then the first declaration below works fine, but the second one is probably an accident and raises an exception on most machines because it is requesting a huge array (indexed from the smallest integer to the largest one):
Array_1 : Small_Array;
Array_2 : Large_Array;
Array Attributes
guideline
- 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
subtype Name_String is String (1 .. Name_Length);
File_Path : Name_String := (others => ' ');
...
for I in File_Path'Range loop
...
end loop;
rationale
In the example above, it is better to use Name_String'Range
in the
for
loop than to use Name_String_Size
, Name_String'First .. Name_String'Last
, or 1 .. 30
because it is clearer, less error-prone,
and less dependent on the definitions of Name_String
and
Name_String_Size
. If Name_String
is changed to have a different
index type or if the bounds of the array are changed, this will still
work correctly. This enhances program reliability.
Parenthetical Expressions
guideline
- 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
(1.5 * X**2)/A - (6.5*X + 47.0)
2*I + 4*Y + 8*Z + C
rationale
The Ada rules of operator precedence are defined in the Ada Reference Manual (1995, §4.5) and follow the same commonly accepted precedence of algebraic operators. The strong typing facility in Ada combined with the common precedence rules make many parentheses unnecessary. However, when an uncommon combination of operators occurs, it may be helpful to add parentheses even when the precedence rules apply. The expression:
5 + ((Y ** 3) mod 10)
is clearer, and equivalent to:
5 + Y**3 mod 10
The rules of evaluation do specify left to right evaluation for operators with the same precedence level. However, it is the most commonly overlooked rule of evaluation when checking expressions for correctness.
Positive Forms of Logic
guideline
- 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
Use:
if Operator_Missing then
rather than either:
if not Operator_Found then
or:
if not Operator_Missing then
rationale
Relational expressions can be more readable and understandable when stated in a positive form. As an aid in choosing the name, consider that the most frequently used branch in a conditional construct should be encountered first.
exceptions
There are cases in which the negative form is unavoidable. If the relational expression better reflects what is going on in the code, then inverting the test to adhere to this guideline is not recommended.
Short Circuit Forms of the Logical Operators
guideline
- 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
Use:
if Y /= 0 or else (X/Y) /= 10 then
or:
if Y /= 0 then
if (X/Y) /= 10 then
rather than either:
if Y /= 0 and (X/Y) /= 10 then
or:
if (X/Y) /= 10 then
to avoid Constraint_Error.
Use:
if Target /= null and then Target.Distance < Threshold then
rather than:
if Target.Distance < Threshold then
to avoid referencing a field in a nonexistent object.
rationale
The use of short-circuit control forms prevents a class of
data-dependent errors or exceptions that can occur as a result of
expression evaluation. The short-circuit forms guarantee an order of
evaluation and an exit
from the sequence of relational expressions as
soon as the expression's result can be determined.
In the absence of short-circuit forms, Ada does not provide a guarantee
of the order of expression evaluation, nor does the language guarantee
that evaluation of a relational expression is abandoned when it becomes
clear that it evaluates to False
(for and
) or True
(for or
).
notes
If it is important that all parts of a given expression always be evaluated, the expression probably violates Guideline 4.1.4 , which limits side-effects in functions.
Accuracy of Operations With Real Operands
guideline
- Use
<=
and>=
in relational expressions with real operands instead of=
.
example
Current_Temperature : Temperature := 0.0;
Temperature_Increment : Temperature := 1.0 / 3.0;
Maximum_Temperature : constant := 100.0;
...
loop
...
Current_Temperature :=
Current_Temperature + Temperature_Increment;
...
exit when Current_Temperature >= Maximum_Temperature;
...
end loop;
rationale
Fixed- and floating-point values, even if derived from similar expressions, may not be exactly equal. The imprecise, finite representations of real numbers in hardware always have round-off errors so that any variation in the construction path or history of two real numbers has the potential for resulting in different numbers, even when the paths or histories are mathematically equivalent.
The Ada definition of model intervals also means that the use of <=
is
more portable than either <
or =
.
notes
Floating-point arithmetic is treated in Guideline 7.2.7 .
exceptions
If your application must test for an exact value of a real number (e.g.,
testing the precision of the arithmetic on a certain machine), then the
=
would have to be used. But never use =
on real operands as a
condition to exit a loop .
Statements
Careless or convoluted use of statements can make a program hard to read and maintain even if its global structure is well organized. You should strive for simple and consistent use of statements to achieve clarity of local program structure. Some of the guidelines in this section counsel use or avoidance of particular statements. As pointed out in the individual guidelines, rigid adherence to those guidelines would be excessive, but experience has shown that they generally lead to code with improved reliability and maintainability.
Nesting
guideline
- 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
- Do not nest expressions or control structures beyond a nesting level of five.
example
The following section of code:
if not Condition_1 then
if Condition_2 then
Action_A;
else -- not Condition_2
Action_B;
end if;
else -- Condition_1
Action_C;
end if;
can be rewritten more clearly and with less nesting as:
if Condition_1 then
Action_C;
elsif Condition_2 then
Action_A;
else -- not (Condition_1 or Condition_2)
Action_B;
end if;
rationale
Deeply nested structures are confusing, difficult to understand, and
difficult to maintain. The problem lies in the difficulty of determining
what part of a program is contained at any given level. For expressions,
this is important in achieving the correct placement of balanced
grouping symbols and in achieving the desired operator precedence. For
control structures, the question involves what part is controlled.
Specifically, is a given statement at the proper level of nesting, that
is, is it too deeply or too shallowly nested, or is the given statement
associated with the proper choice, for example, for if
or case
statements? Indentation helps, but it is not a panacea. Visually
inspecting alignment of indented code (mainly intermediate levels) is an
uncertain job at best. To minimize the complexity of the code, keep the
maximum number of nesting levels between three and five.
notes
Ask yourself the following questions to help you simplify the code:
- 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 acase
statement? - Am I using
else if
where I could be usingelsif
? - Can I reorder the conditional expressions controlling this nested structure?
- Is there a different design that would be simpler?
exceptions
If deep nesting is required frequently, there may be overall design decisions for the code that should be changed. Some algorithms require deeply nested loops and segments controlled by conditional branches. Their continued use can be ascribed to their efficiency, familiarity, and time-proven utility. When nesting is required, proceed cautiously and take special care with the choice of identifiers and loop and block names.
Slices
guideline
- Use slices rather than a loop to copy part of an array.
example
First : constant Index := Index'First;
Second : constant Index := Index'Succ(First);
Third : constant Index := Index'Succ(Second);
type Vector is array (Index range <>) of Element;
subtype Column_Vector is Vector (Index);
type Square_Matrix is array (Index) of Column_Vector;
subtype Small_Range is Index range First .. Third;
subtype Diagonals is Vector (Small_Range);
type Tri_Diagonal is array (Index) of Diagonals;
Markov_Probabilities : Square_Matrix;
Diagonal_Data : Tri_Diagonal;
...
-- Remove diagonal and off diagonal elements.
Diagonal_Data(Index'First)(First) := Null_Value;
Diagonal_Data(Index'First)(Second .. Third) :=
Markov_Probabilities(Index'First)(First .. Second);
for I in Second .. Index'Pred(Index'Last) loop
Diagonal_Data(I) :=
Markov_Probabilities(I)(Index'Pred(I) .. Index'Succ(I));
end loop;
Diagonal_Data(Index'Last)(First .. Second) :=
Markov_Probabilities(Index'Last)(Index'Pred(Index'Last) .. Index'Last);
Diagonal_Data(Index'Last)(Third) := Null_Value;
rationale
An assignment statement with slices is simpler and clearer than a loop and helps the reader see the intended action. See also Guideline 10.5.7 regarding possible performance issues of slice assignments versus loops.
Case Statements
guideline
- Minimize the use of an
others
choice in acase
statement. - Do not use ranges of enumeration literals in
case
statements. - Use
case
statements rather thanif/elsif
statements, wherever possible. - Use type extension and dispatching rather than
case
statements if, possible.
example
type Color is (Red, Green, Blue, Purple);
Car_Color : Color := Red;
...
case Car_Color is
when Red .. Blue => ...
when Purple => ...
end case; -- Car_Color
Now consider a change in the type:
type Color is (Red, Yellow, Green, Blue, Purple);
This change may have an unnoticed and undesired effect in the case
statement. If the choices had been enumerated explicitly, as when Red | Green | Blue =>
instead of when
Red .. Blue =>
, then the case
statement would not have compiled. This would have forced the maintainer
to make a conscious decision about what to do in the case of Yellow
.
In the following example, assume that a menu has been posted, and the
user is expected to enter one of the four choices. Assume that
User_Choice
is declared as a Character
and that Terminal_IO.Get
handles errors in user input. The less readable alternative with the
if/elsif
statement is shown after the case
statement:
Do_Menu_Choices_1:
loop
...
case User_Choice is
when 'A' => Item := Terminal_IO.Get ("Item to add");
when 'D' => Item := Terminal_IO.Get ("Item to delete");
when 'M' => Item := Terminal_IO.Get ("Item to modify");
when 'Q' => exit Do_Menu_Choices_1;
when others => -- error has already been signaled to user
null;
end case;
end loop Do_Menu_Choices_1;
Do_Menu_Choices_2:
loop
...
if User_Choice = 'A' then
Item := Terminal_IO.Get ("Item to add");
elsif User_Choice = 'D' then
Item := Terminal_IO.Get ("Item to delete");
elsif User_Choice = 'M' then
Item := Terminal_IO.Get ("Item to modify");
elsif User_Choice = 'Q' then
exit Do_Menu_Choices_2;
end if;
end loop Do_Menu_Choices_2;
rationale
All possible values for an object should be known and should be assigned
specific actions. Use of an others
clause may prevent the developer
from carefully considering the actions for each value. A compiler warns
the user about omitted values if an others
clause is not used.
You may not be able to avoid the use of others
in a case
statement
when the subtype of the case expression has many values, for example,
universal_integer
, Wide_Character
, or Character
). If your choice
of values is small compared to the range of the subtype, you should
consider using an if/elsif
statement. Note that you must supply an
others
alternative when your case
expression is of a generic type.
Each possible value should be explicitly enumerated. Ranges can be
dangerous because of the possibility that the range could change and the
case
statement may not be reexamined. If you have declared a subtype
to correspond to the range of interest, you can consider using this
named subtype.
In many instances, case
statements enhance the readability of the
code. See Guideline 10.5.3 for a discussion of the performance
considerations. In many implementations, case
statements may be more
efficient.
Type extension and dispatching ease the maintenance burden when you add a new variant to a data structure. See also Guidelines 5.4.2 and 5.4.4 .
notes
Ranges that are needed in case
statements can use constrained subtypes
to enhance maintainability. It is easier to maintain because the
declaration of the range can be placed where it is logically part of the
abstraction, not buried in a case
statement in the executable code:
subtype Lower_Case is Character range 'a' .. 'z';
subtype Upper_Case is Character range 'A' .. 'Z';
subtype Control is Character range Ada.Characters.Latin_1.NUL ..
Ada.Characters.Latin_1.US;
subtype Numbers is Character range '0' .. '9';
...
case Input_Char is
when Lower_Case => Capitalize(Input_Char);
when Upper_Case => null;
when Control => raise Invalid_Input;
when Numbers => null;
...
end case;
exceptions
It is acceptable to use ranges for possible values only when the user is
certain that new values will never be inserted among the old ones, as
for example, in the range of ASCII characters: 'a' .. 'z'
.
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.