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 SyntaxEdit
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 exitstatement.
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;
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.
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;
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.
- Use loop names on all exit statements from nested loops.
See the example in 5.1.1 .
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 StatementsEdit
- Include the defining program unit name at the end of a package specification and body.
- Include the defining identifier at the end of a task specification and body.
- Include the entry identifier at the end of an acceptstatement.
- Include the designator at the end of a subprogram body.
- Include the defining identifier at the end of a protected unit declaration.
------------------------------------------------------------------------ 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; ------------------------------------------------------------------------
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 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.
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.
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 SubtypesEdit
- 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.
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);
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.
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.
------------------------------------------------------------------------ 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; ------------------------------------------------------------------------
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.
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 TypesEdit
- 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) :
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;
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 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;
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 DataEdit
- 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 record Name : Common_Name; Plant : Propulsion_Method; Length : Feet; Beam : Feet; Draft : Feet; end record; 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 DataEdit
- 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 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; ...
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.
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 record Row : Local_Float; Column : Local_Float; end record; type Window is record Top_Left : Coordinate; Bottom_Right : Coordinate; end record;
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 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
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;
See also 6.3.2 for variations on these problems. A dynamically allocated object is an object created by the execution of an allocator (new). Allocated objects referenced by access variables allow you to generate aliases , which are multiple references to the same object. Anomalous behavior can arise when you reference a deallocated object by another name. This is called a dangling reference. Totally disassociating a still-valid object from all names is called dropping a pointer. A dynamically allocated object that is not associated with a name cannot be referenced or explicitly deallocated.
A dropped pointer depends on an implicit memory manager for reclamation of space. It also raises questions for the reader as to whether the loss of access to the object was intended or accidental.
An Ada environment is not required to provide deallocation of dynamically allocated objects. If provided, it may be provided implicitly (objects are deallocated when their access type goes out of scope), explicitly (objects are deallocated when Ada.Unchecked_Deallocation is called), or both. To increase the likelihood of the storage space being reclaimed, it is best to call Ada.Unchecked_Deallocation explicitly for each dynamically created object when you are finished using it. Calls to Ada.Unchecked_Deallocation also document a deliberate decision to abandon an object, making the code easier to read and understand. To be absolutely certain that space is reclaimed and reused, manage your own "free list." Keep track of which objects you are finished with, and reuse them instead of dynamically allocating new objects later.
The dangers of dangling references are that you may attempt to use them, thereby accessing memory that you have released to the memory manager and that may have been subsequently allocated for another purpose in another part of your program. When you read from such memory, unexpected errors may occur because the other part of your program may have previously written totally unrelated data there. Even worse, when you write to such memory you can cause errors in an apparently unrelated part of the code by changing values of variables dynamically allocated by that code. This type of error can be very difficult to find. Finally, such errors may be triggered in parts of your environment that you did not write, for example, in the memory management system itself, which may dynamically allocate memory to keep records about your dynamically allocated memory.
Keep in mind that any unreset component of a record or array can also be a dangling reference or carry a bit pattern representing inconsistent data. Components of an access type are always initialized by default to null; however, you should not rely on this default initialization. To enhance readability and maintainability, you should include explicit initialization.
Whenever you use dynamic allocation, it is possible to run out of space. Ada provides a facility (a length clause) for requesting the size of the pool of allocation space at compile time. Anticipate that you can still run out at run time. Prepare handlers for the exception Storage_Error, and consider carefully what alternatives you may be able to include in the program for each such situation.
There is a school of thought that dictates avoidance of all dynamic allocation. It is largely based on the fear of running out of memory during execution. Facilities, such as length clauses and exception handlers for Storage_Error, provide explicit control over memory partitioning and error recovery, making this fear unfounded.
When implementing a complex data structure (tree, list, sparse matrices, etc.), you often use access types. If you are not careful, you can consume all your storage with these dynamically allocated objects. You could export a deallocate operation, but it is impossible to ensure that it is called at the proper places; you are, in effect, trusting the clients. If you derive from controlled types (see 8.3.3 , and 9.2.3 for more information), you can use finalization to deal with deallocation of dynamic data, thus avoiding storage exhaustion. User-defined storage pools give better control over the allocation policy.
A related but distinct issue is that of shared versus copy semantics: even if the data structure is implemented using access types, you do not necessarily want shared semantics. In some instances you really want := to create a copy, not a new reference, and you really want = to compare the contents, not the reference. You should implement your structure as a controlled type. If you want copy semantics, you can redefine Adjust to perform a deep copy and = to perform a comparison on the contents. You can also redefine Finalize to make sure that when an object is finalized (i.e., goes out of scope) the dynamically allocated elements are chained on a free list (or deallocated by Ada.Unchecked_Deallocation).
The implicit use of dynamic (heap) storage by an Ada program during execution poses significant risks that software failures may occur. An Ada run-time environment may use implicit dynamic (heap) storage in association with composite objects, dynamically created tasks, and catenation. Often, the algorithms used to manage the dynamic allocation and reclamation of heap storage cause fragmentation or leakage, which can lead to storage exhaustion. It is usually very difficult or impossible to recover from storage exhaustion or Storage_Error without reloading and restarting the Ada program. It would be very restrictive to avoid all uses of implicit allocation. On the other hand, preventing both explicit and implicit deallocation significantly reduces the risks of fragmentation and leakage without overly restricting your use of composite objects, access values, task objects, and catenation.
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.
- 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) 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;
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 and and or.
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;
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.
- 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.
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;
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.
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 '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.
subtype Name_String is String (1 .. Name_Length); File_Path : Name_String := (others => ' '); ... for I in File_Path'Range loop ... end loop;
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.
- 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 LogicEdit
- 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 OperatorsEdit
- 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
to avoid Constraint_Error.
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 False (for and) or True (for or).
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 OperandsEdit
- Use <= and >= 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; ... loop ... Current_Temperature := Current_Temperature + Temperature_Increment; ... exit when Current_Temperature >= Maximum_Temperature; ... end loop;
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 =.
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 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;
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.
Ask yourself the following questions to help you simplify the code:
- Can some part of the expression be put into a constant or variable?
- Does some part of the lower nested control structures represent a significant and, perhaps, reusable computation that I can factor into a subprogram ?
- Can I convert these nested if statements into a case statement?
- Am I using else if where I could be using elsif ?
- Can I reorder the conditional expressions controlling this nested structure?
- Is there a different design that would be simpler?
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 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;
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 others choice in a case statement.
- Do not use ranges of enumeration literals in case statements.
- Use case statements rather than if/elsif statements, wherever possible.
- Use type extension and dispatching rather than case statements if, possible.
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;
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 .
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;
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'.