Ada Style Guide/Print version
This is the print version of Ada Style Guide You won't see this message or any elements not part of the book's content when you print or preview this page. |
Preface
This style guide is an update to the Ada 95 Quality and Style Guide to reflect the latest update to the Ada language, commonly called Ada 2012. The purpose of this guide is to help computer professionals produce better Ada programs by identifying a set of stylistic guidelines that will directly impact the quality of their Ada programs. This style guide is not intended to replace the Ada Reference Manual, or the Rationale, or to serve as a tutorial for the Ada programming language.
The style guide is divided into chapters that map to the major decisions that each programmer addresses when creating high-quality, reliable, reusable, and portable Ada software. Some overlap exists in the chapters because not all programming decisions can be made independently. Individual chapters address source code presentation, readability, program structure, programming practice, concurrency, portability, reusability, performance, and a new chapter on object-oriented features.
Each chapter is divided into guidelines, using a format that supports wide usage because its content is both prescriptive and tailorable. Each guideline consists of a concise statement of the principles that should be followed and a rationale explaining why the guideline is important. The guidelines also provide usage examples, in addition to possible exceptions to applying the guidelines. Many of the guidelines are specific enough to be adopted as corporate or project programming standards. Others require a managerial decision on a particular instantiation before they can be used as standards. In such cases, a sample instantiation is presented and used throughout the examples.
Background
The Ada Joint Program Office (AJPO) funded the Ada 95 style guide, which was created by merging a set of guidelines for using Ada 95 with modifications to the original Ada Quality and Style: Guidelines for Professional Programmers, version 02.01.01 (AQ&S 83) (Software Productivity Consortium 1992), developed to support Ada 83. The Ada 95 guidelines are based on the wealth of data available from the Ada 9X Project, the AJPO library, and the Ada community at large. The Software Productivity Consortium's (Consortium's) technical staff authored the update and the Advanced Research Projects Agency (ARPA) participated in the update effort.
The preexisting AQ&S 83 presented a set of guidelines to help the programmer make disciplined use of Ada's features. In 1992, the Consortium completed the version 2.1 update to the style guide under contract to the AJPO. The AJPO referred to that style guide as "the suggested style guide for all DoD programs."
As yet, there is no formal update of the style guide for the Ada 2005 or the Ada 2012 language revision. During an extended discussion at the SIGAda 2008 conference of the style guide via-a-vis the Ada 2005 update and its continuing relevance, several attendees volunteered to bring the guide to wikibooks to encourage collaborative updating of the guide by putting the update in the hands of practitioners of the language. This is in part made possible under the terms of the DoD Ada Joint Program Office's royalty-free, worldwide, non-exclusive, irrevocable license for unlimited use of the material in the Ada 83 and 95 style guides.
Introduction
Introduction
Style is an often overlooked but very critical attribute of writing. The style of writing directly impacts the readability and understandability of the end product. The style of programming, as the writing of source code in a computer language, also suffers from this neglect. Programs need to be readable and understandable by humans, not just comparable by machines. This requirement is important in the creation of quality products that not only meet user needs but also can be developed on schedule and within estimated cost. This book is intended to help the computer professional produce better Ada programs. It presents a set of specific stylistic guidelines for using the powerful features of Ada 95 (Ada Reference Manual 1995) in a disciplined manner.
Each guideline consists of a concise statement of the principles that should be followed and a rationale for following the guideline. In most cases, an example of the use of the guideline is provided, and, in some cases, a further example is included to show the consequences of violating the guideline. Possible exceptions to the application of the guideline are explicitly noted, and further explanatory notes are provided, where appropriate. In some cases, an instantiation is provided to show more specific guidance that could be enforced as a standard. In selected cases, automation notes discuss how one could automate enforcement of the guideline.
Ada was designed to support the development of high-quality, reliable, reusable, and portable software. For a number of reasons, no programming language can ensure the achievement of these desirable objectives on its own. For example, programming must be embedded in a disciplined development process that addresses requirements analysis, design, implementation, verification, validation, and maintenance in an organized way. The use of the language must conform to good programming practices based on well-established software engineering principles. This book is intended to help bridge the gap between these principles and the actual practice of programming in Ada.
Many of the guidelines in this book are designed to promote clear source text. The goal of these guidelines is to improve the ease of program evolution, adaptation, and maintenance. Understandable source text is more likely to be correct and reliable. Easy adaptation requires a thorough understanding of the software; this is considerably facilitated by clarity. Effective code adaptation is a prerequisite to code reuse, a technique that has the potential for drastic reductions in system development cost. Finally, because maintenance (really evolution) is a costly process that continues throughout the life of a system, clarity plays a major role in keeping maintenance costs down. Over the entire life cycle, code has to be read and understood far more often than it is written; thus, the investment in writing readable, understandable code is worthwhile.
The remaining sections of this introduction discuss the organization of this book and how the material presented can be used by people in different roles, including new Ada programmers, experienced Ada programmers, object-oriented programmers, software project managers, contracting agencies, standards setting organizations, and planners of the transition to Ada 95 from existing Ada 83 (Ada Reference Manual 1983) programs.
Organization of this Book
The format of this book follows the well-received guideline format of the Ada Quality and Style: Guidelines for Professional Programmer's, version 02.01.01 (AQ&S 83) (Software Productivity Consortium 1992). The style guide is divided into sections that map to the major decisions that each programmer must make when creating high-quality, reliable, reusable, and portable Ada software. Some overlap exists in the sections because not all programming decisions can be made independently.
Individual chapters address source code presentation, readability, program structure, programming practices, concurrency, portability, reusability, and performance, and a new chapter addresses object-oriented features. Each chapter ends with a summary of the guidelines it contains. The last chapter shows a complete implementation of the Dining Philosophers example, provided by Dr. Michael B. Feldman and Mr. Bjorn Kallberg. Many of the guidelines in this book were used to create this example. An appendix provides a cross-reference matrix between the Ada Reference Manual (1995) sections and the guidelines in this style guide.
This book is written using the general software engineering vocabulary developed over the last 20 years. Software engineering is a rapidly evolving discipline with relatively new concepts and terminology. However, to establish a common frame of reference, needed definitions are extracted from the Ada Reference Manual (1995) and Rationale (1995).
Throughout the book, references are made to other sources of information about Ada style and other Ada issues. The references are listed at the end of the book. A bibliography is also provided.
In this book, the term "Ada" refers to the latest Ada standard, released in February 1995 (sometimes also known as Ada 95). References to the earlier Ada standard are clearly denoted as "Ada 83."
Source Code Presentation and Readability
Chapters 2 and 3 directly address the issues of creating clear, readable, and understandable source text. Chapter 2 focuses on code formatting, and Chapter 3 addresses issues of use of comments, naming conventions, and types.
There are two main aspects of code clarity: (1) careful and consistent layout of the source text on the page or the screen, covered by Chapter 2, that can enhance readability dramatically; (2) careful attention to the structure of code, covered by Chapter 3, that can make the code easier to understand. This is true both on the small scale (e.g., by careful choice of identifier names or by disciplined use of loops) and on the large scale (e.g., by proper use of packages). These guidelines treat both layout and structure.
Code formatting and naming convention preferences tend to be very personal. You must balance your personal likes and dislikes with those of other engineers on the project so that you can agree to a consistent set of conventions that the whole project team will follow. Automatic code formatters can help in enforcing this kind of consistency.
Program Structure
Chapter 4 addresses overall program structure. Proper structure improves program clarity. This is analogous to readability on lower levels and includes issues of high-level structure, in particular the use of packages and child packages, visibility, and exceptions. The majority of the guidelines in this chapter are concerned with the application of sound software engineering principles, such as information hiding, abstraction, encapsulation, and separation of concerns.
Programming Practices
Chapter 5 presents guidelines that define consistent and logical language feature usage. These guidelines address optional parts of the syntax, types, data structures, expressions, statements, visibility, exceptions, and erroneous execution.
Concurrency
Chapter 6 defines the correct use of concurrency to develop predictable, reliable, reusable, and portable software. The topics include tasking, protected units, communication, and termination. One major area of enhancement of the Ada language has been better support for shared data. The task mechanism had been the only available approach to protecting shared data. The guidelines in this chapter support the use of protected types to encapsulate and synchronize access to shared data.
Portability and Reusability
Chapters 7 and 8 address issues of designing for change from slightly different perspectives. Chapter 7 addresses the fundamentals of portability, the ease of changing software from one computer system or environment to another, and the impact of specific feature usage on portability. Chapter 8 addresses code reusability, the extent to which code can be used in different applications with minimal change.
The portability guidelines discussed in Chapter 7 need careful attention. Adherence to them is important even if the need to port the resulting software is not currently foreseen. Following the guidelines improves the potential reusability of the resulting code in projects that use different Ada implementations. You should insist that when particular project needs force the relaxation of some of the portability guidelines, nonportable features of the source text are prominently indicated.
The reusability guidelines given in Chapter 8 are based on the principles of encapsulation and design for change. These guidelines stress that understanding and clarity, robustness, adaptability, and independence are useful and desirable, even when reuse is not expected, because the resulting code is more resistant to both planned and unplanned change.
Object-Oriented Features
Chapter 9 defines a set of guidelines in common objected-oriented terms that exploit some of the features of Ada 95 that are not in Ada 83. The guidelines discuss the use of the new Ada features of type extension (tagged types), abstract tagged types, and abstract subprograms to implement single inheritance, multiple inheritance, and polymorphism.
Performance
Chapter 10 defines a set of guidelines intended to enhance performance. It is recognized that some approaches to performance are at odds with maintainability and portability. Most of the guidelines in this chapter read ". . . when measured performance indicates." "Indicates" means that you have determined that the benefit in increased performance to your application in your environment outweighs the negative side effects on understandability, maintainability, and portability of the resulting code.
How To Use This Book
This book is intended for those involved in the actual development of software systems written in Ada. The following sections discuss how to make the most effective use of the material presented. Readers with different levels of Ada experience or different roles in a software project will need to use the book in different ways.
There are a number of ways in which this book can be used: as a guide to good Ada style; as a comprehensive list of guidelines that will contribute to better Ada programs; or as a reference work to consult for usage examples of and design-tradeoff discussion on specific features of the language. The book contains many guidelines, some of which are quite complex. Learning them all at the same time should not be necessary; it is unlikely that you will be using all the features of the language at once. However, it is recommended that all programmers (and, where possible, other Ada project staff) make an effort to read and understand Chapters 2, 3, 4, and Chapter 5 up to Section 5.7. Some of the material is quite difficult (e.g., Section 4.2, which discusses visibility), but it covers issues that are fundamental to the effective use of Ada and is important for any software professional involved in building Ada systems.
This book is not intended as an introductory text on Ada or as a complete manual of the Ada language. It is assumed that you already know the syntax of Ada and have a rudimentary understanding of the semantics. With such a background, you should find the guidelines useful, informative, and often enlightening.
If you are learning Ada, you should equip yourself with a comprehensive introduction to the language. Two good introductory texts on Ada 83 are Barnes (1989) and Cohen (1986). Both authors have published new books that cover Ada 95 (Barnes 1996, Cohen 1996). Once you become familiar with these texts, you are encouraged to use them in conjunction with Rationale (1995). The Ada Reference Manual (1995) should be regarded as a companion to these books. The majority of guidelines reference the sections of the Ada Reference Manual (1995) that define the language features being discussed. Appendix A cross references sections of the Ada Language Reference Manual to the guidelines.
To The New Ada Programmer
At first sight, Ada offers a bewildering variety of features. It is a powerful tool intended to solve difficult problems, and almost every feature has a legitimate application in some context. This makes it especially important to use Ada's features in a disciplined and organized way. Following the guidelines can make learning Ada easier and help you to master its apparent complexity. From the beginning, you can write programs that exploit the best features of the language in the way that the designers intended.
Programmers experienced in using other programming languages are often tempted to use Ada as if it were their familiar language but with irritating syntactic differences. This pitfall should be avoided at all costs; it can lead to convoluted code that subverts exactly those aspects of Ada that make it so suitable for building high-quality systems. You must learn to "think Ada." Following the guidelines in this book and reading the examples of their use will help you to do this as quickly and painlessly as possible.
To some degree, novice programmers learning Ada have an advantage. Following the guidelines from the beginning helps in developing a clear programming style that effectively exploits the language. If you are in this category, it is recommended that you adopt the guidelines for those exercises you perform as part of learning Ada. Initially, developing sound programming habits by concentrating on the guidelines themselves and their supporting examples is more important than understanding the rationale for each guideline.
The rationale for many of the guidelines helps experienced programmers understand and accept the suggestions presented in the guideline. Some of the guidelines themselves are also written for the experienced programmer who must make engineering tradeoffs. This is especially true in the areas of portability, reusability, and performance. These more difficult guidelines and rationale will make you aware of the issues affecting each programming decision. You can then use that awareness to recognize the engineering tradeoffs that you will eventually be asked to make when you are the experienced Ada programmer.
To The Experienced Ada Programmer
As an experienced Ada programmer, you are already writing code that conforms to many of the guidelines in this book. In some areas, however, you may have adopted a personal programming style that differs from that presented here, and you might be reluctant to change. Carefully review those guidelines that are inconsistent with your current style, make sure that you understand their rationale, and consider adopting them. The overall set of guidelines in this book embodies a consistent approach to producing high-quality programs that would be weakened by too many exceptions.
Consistency is another important reason for general adoption of common guidelines. If all the staff of a project write source text in the same style, many critical project activities are easier. Consistent code simplifies formal and informal code reviews, system integration, code reuse within a project, and the provision and application of supporting tools. In practice, corporate or project standards may require deviations from the guidelines to be explicitly commented, so adopting a nonstandard approach may require extra work.
Some of the guidelines in this book, particularly in the chapters on concurrency, portability, reusability, object-oriented features, and performance, focus on design tradeoffs. These guidelines ask you to consider whether using an Ada feature is an appropriate design decision for your application. There are often several ways to implement a particular design decision, and these guidelines discuss the tradeoffs you should consider in making your decision.
To Experienced Object-Oriented Programmers
As an experienced object-oriented programmer, you will appreciate the effort that has gone into elegantly extending the Ada language to include powerful object-oriented features. These new features are integrated tightly with the existing language features and vocabulary. This book is intentionally written to provide a view from the perspective of style; therefore, Ada object-oriented features are used throughout the book. Disciplined use of these features will promote programs that are easier to read and modify. These features also give you flexibility in building reusable components. Chapter 9 addresses object-oriented programming and the issues of inheritance and polymorphism. Earlier chapters cross reference the Chapter 9 guidelines.
You will find it easier to take advantage of many of the concepts in Chapter 9 if you have done an object-oriented design. The results of an object-oriented design would include a set of meaningful abstractions and hierarchy of classes. The abstractions need to include the definition of the design objects, including structure and state, the operations on the objects, and the intended encapsulation for each object. The details on designing these abstractions and the hierarchy of classes are beyond the scope of this book. A number of good sources exist for this detail, including Rumbaugh et al. (1991), Jacobson et al. (1992), the ADARTS Guidebook (Software Productivity Consortium 1993), and Booch (1994).
To The Software Project Manager
Technical management plays a key role in ensuring that the software produced in the course of a project is correct, reliable, maintainable, and portable. Management must create a project-wide commitment to the production of high-quality code; define project-specific coding standards and guidelines; foster an understanding of why uniform adherence to the chosen coding standards is critical to product quality; and establish policies and procedures to check and enforce that adherence. The guidelines contained in this book can aid such an effort.
An important activity for managers is the definition of coding standards for a project or organization. These guidelines do not, in themselves, constitute a complete set of standards; however, they can serve as a basis for standards. Several guidelines indicate a range of decisions, but they do not prescribe a particular decision. For example, the second guideline in the book (Guideline 2.1.2) advocates using a consistent number of spaces for indentation and indicates in the rationale that two to four spaces would be reasonable. With your senior technical staff, you should review each such guideline and arrive at a decision about its instantiation that will constitute your project or organizational standard.
Two other areas require managerial decisions about standardization. Guideline 3.1.4 advises you to avoid arbitrary abbreviations in object or unit names. You should prepare a glossary of acceptable abbreviations for a project that allows the use of shorter versions of application-specific terms (e.g., FFT for Fast Fourier Transform or SPN for Stochastic Petri Net). You should keep this glossary short and restrict it to terms that need to be used frequently as part of names. Having to refer continually to an extensive glossary to understand source text makes it hard to read.
The portability guidelines given in Chapter 7 need careful attention. Adherence to them is important even if the need to port the resulting software is not currently foreseen. Following the guidelines improves the potential reusability of the resulting code in projects that use different Ada implementations. You should insist that when particular project needs force the relaxation of some of the portability guidelines, nonportable features of the source text are prominently indicated. Observing the Chapter 7 guidelines requires definition and standardization of project- or organization-specific numeric types to be used in place of the (potentially nonportable) predefined numeric types.
Your decisions on standardization issues should be incorporated in a project or organization coding standards document. With coding standards in place, you need to ensure adherence to them. Gaining the wholehearted commitment of your programming staff to use the standards is critical. Given this commitment and the example of high-quality Ada being produced by your programmers, it will be far easier to conduct effective formal code reviews that check compliance to project standards.
Some general issues concerning the management of Ada projects are discussed by Hefley, et al. (1992).
To Contracting Agencies And Standards Organizations
Many of the guidelines presented here are specific enough to be adopted as corporate or project programming standards. Others require a managerial decision on a particular instantiation before they can be used as standards. In such cases, a sample instantiation is presented and used throughout the examples. Such instantiations should be recognized as weaker recommendations than the guidelines themselves. In some cases, where the examples are extracted from a published work, the author's style is used unchanged.
Other guidelines presented in this book are intentionally phrased in terms of design choices to consider. These guidelines cannot be instantiated as hard-and-fast rules that a project must follow. For example, you should not interpret Guidelines 6.1.1 and 6.1.2 to mean that a project is forbidden to use tasks. Rather, these guidelines are intended to help the designer make the tradeoffs between using protected objects and tasks, thus leading the designer to make a more informed choice between these features.
The guidelines in this document are not intended to stand alone as a standard. In some cases, it is not clear that a guideline could be enforced because it is only intended to make the engineer aware of tradeoffs. In other cases, a choice still remains about a guideline, such as how many spaces to use for each level of indentation.
When a guideline is too general to show an example, the "instantiation" section of each guideline contains more specific guidelines. These instantiations can be considered a standard and are more likely to be enforceable. Any organization that attempts to extract standards from this document needs to evaluate the complete context. Each guideline works best when related guidelines are practiced. In isolation, a guideline may have little or no benefit.
To Ada 83 To Ada 95 Transition Planners
Transitioning issues fall into two major categories: the incompatibilities between the languages, in particular, upward compatibility, and exploitation of new language features.
Upward compatibility of Ada 95 was a major design goal of the language. The small number of incompatibilities between Ada 83 and Ada 95 that are likely to occur in practice are easily overcome (see Ada 95 Rationale [1995] Appendix X entitled Upward Compatibility). Detailed information on compatibility issues can be found in Taylor (1995) and Intermetrics (1995).
The transition planner can gain insight from this book into the exploitation of language features in two ways. First, Table 1 shows the impact of new Ada 95 language features on style guide chapters. Second, Appendix A maps Ada Reference Manual (1995) sections to specific style guidelines.
Typographic Conventions
This style guide uses the following typographic conventions:
- Serif font General presentation of information.
- Italicized serif font Publication titles and emphasis.
- Boldfaced serif font Section headings.
- Boldfaced sans serif font Subheadings for guideline, instantiation, example, rationale, notes, exceptions, automation notes, caution, and subheadings in Summary sections.
- Typewriter font:
Syntax of code.
Source Code Presentation
Introduction
The physical layout of source text on the page or screen has a strong effect on its readability. This chapter contains source code presentation guidelines intended to make the code more readable.
In addition to the general purpose guidelines, specific recommendations are made in the "instantiation" sections. If you disagree with the specific recommendations, you may want to adopt your own set of conventions that still follow the general purpose guidelines. Above all, be consistent across your entire project.
An entirely consistent layout is hard to achieve or check manually. Therefore, you may prefer to automate layout with a tool for parameterized code formatting or incorporate the guidelines into an automatic coding template. Some of the guidelines and specific recommendations presented in this chapter cannot be enforced by a formatting tool because they are based on the semantics, not the syntax, of the Ada code. More details are given in the "automation notes" sections.
Code Formatting
The "code formatting" of Ada source code affects how the code looks, not what the code does. Topics included here are horizontal spacing, indentation, alignment, pagination, and line length. The most important guideline is to be consistent throughout the compilation unit as well as the project.
Horizontal Spacing
guideline
- Use consistent spacing around delimiters.
- Use the same spacing as you would in regular prose.
instantiation
Specifically, leave at least one blank space in the following places, as shown in the examples throughout this book. More spaces may be required for the vertical alignment recommended in subsequent guidelines.
- Before and after the following delimiters and binary operators:
+ - * / &
< = > /= <= >=
:= => | ..
:
<>
- Outside of the quotes for string (") and character (' ) literals, except where prohibited.
- Outside, but not inside, parentheses.
- After commas (,) and semicolons (;).
Do not leave any blank spaces in the following places, even if this conflicts with the above recommendations.
- After the plus (+) and minus (-) signs when used as unary operators.
- After a function call.
- Inside of label delimiters (<< >>).
- Before and after the exponentiation operator (**), apostrophe ('), and period (.)
- Between multiple consecutive opening or closing parentheses.
- Before commas (,) and semicolons (;).
When superfluous parentheses are omitted because of operator precedence rules, spaces may optionally be removed around the highest precedence operators in that expression.
example
Default_String : constant String :=
"This is the long string returned by" &
" default. It is broken into multiple" &
" Ada source lines for convenience.";
type Signed_Whole_16 is range -2**15 .. 2**15 - 1;
type Address_Area is array (Natural range <>) of Signed_Whole_16;
Register : Address_Area (16#7FF0# .. 16#7FFF#);
Memory : Address_Area ( 0 .. 16#7FEC#);
Register (Pc) := Register (A);
X := Signed_Whole_16 (Radius * Sin (Angle));
Register (Index) := Memory (Base_Address + Index * Element_Length);
Get (Value => Sensor);
Error_Term := 1.0 - (Cos (Theta)**2 + Sin (Theta)**2);
Z := X**3;
Y := C * X + B;
Volume := Length * Width * Height;
rationale
It is a good idea to use white space around delimiters and operators because they are typically short sequences (one or two characters) that can easily get lost among the longer keywords and identifiers. Putting white space around them makes them stand out. Consistency in spacing also helps make the source code easier to scan visually.
However, many of the delimiters (commas, semicolons, parentheses, etc.) are familiar as normal punctuation marks. It is distracting to see them spaced differently in a computer program than in normal text. Therefore, use the same spacing as in text (no spaces before commas and semicolons, no spaces inside parentheses, etc.).
exceptions
The one notable exception is the colon (:). In Ada, it is useful to use the colon as a tabulator or a column separator (see Guideline 2.1.4). In this context, it makes sense to put spaces before and after the colon rather than only after it as in normal text.
automation notes
The guidelines in this section are easily enforced with an automatic code formatter.
Indentation
guideline
- Indent and align nested control structures, continuation lines, and embedded units consistently.
- Distinguish between indentation for nested control structures and for continuation lines.
- Use spaces for indentation, not the tab character (Nissen and Wallis 1984, §2.2).
instantiation
Specifically, the following indentation conventions are recommended, as shown in the examples throughout this book. Note that the minimum indentation is described. More spaces may be required for the vertical alignment recommended in subsequent guidelines.
- Use the recommended paragraphing shown in the Ada Reference Manual (1995).
- Use three spaces as the basic unit of indentation for nesting.
- Use two spaces as the basic unit of indentation for continuation lines.
A label is outdented three spaces:
begin
<<label>>
<statement>
end;
|
<long statement with line break>
<trailing part of same statement>
|
The if statement and the plain loop:
if <condition> then
<statements>
elsif <condition> then
<statements>
else
<statements>
end if;
|
<name>:
loop
<statements>
exit when <condition>;
<statements>
end loop <name>;
|
Loops with the for and while iteration schemes:
<name>:
for <scheme> loop
<statements>
end loop <name>;
|
<name>:
while <condition> loop
<statements>
end loop <name>;
|
The block and the case statement as recommended in the Ada Reference Manual (1995):
<name>:
declare
<declarations>
begin
<statements>
exception
when <choice> =>
<statements>
when others =>
<statements>
end <name>;
|
case <expression> is
when <choice> =>
<statements>
when <choice> =>
<statements>
when others =>
<statements>
end case; --<comment>
|
These case statements save space over the Ada Reference Manual (1995) recommendation and depend on very short statement lists, respectively. Whichever you choose, be consistent:
case <expression> is
when <choice> =>
<statements>
when <choice> =>
<statements>
when others =>
<statements>
end case;
|
case <expression> is
when <choice> => <statements>
<statements>
when <choice> => <statements>
when others => <statements>
end case;
|
The various forms of selective accept and the timed and conditional entry calls:
select
when <guard> =>
<accept statement>
<statements>
or
<accept statement>
<statements>
or
when <guard> =>
delay <interval>;
<statements>
or
when <guard> =>
terminate;
else
<statements>
end select;
|
select
<entry call>;
<statements>
or
delay <interval>;
<statements>
end select;
select
<entry call>;
<statements>
else
<statements>
end select;
select
<triggering alternative>
then abort
<abortable part>
end select;
|
The accept statement:
accept <specification> do
<statements>
end <name>;
|
separate (<parent unit>)
<proper body>
|
A subunit:
separate (<parent unit>)
<proper body>
end <name>;
Proper bodies of program units:
procedure <specification> is
<declarations>
begin
<statements>
exception
when <choice> =>
<statements>
end <name>;
function <specification>
return <type name> is
<declarations>
begin
<statements>
exception
when <choice> =>
<statements>
end <name>;
|
package body <name> is
<declarations>
begin
<statements>
exception
when <choice>=>
<statements>
end <name>;
task body <name> is
<declarations>
begin
<statements>
exception
when <choice>=>
<statements>
end <name>;
|
Context clauses on compilation units are arranged as a table. Generic formal parameters do not obscure the unit itself. Function, package, and task specifications use standard indentation:
with <name>; use <name>;
with <name>;
with <name>;
<compilation unit>
generic
<formal parameters>
<compilation unit>
|
function <specification>
return <type>;
package <name> is
<declarations>
private
<declarations>
end <name>;
task type <name> is
<entry declarations>
end <name>;
|
Instantiations of generic units and record indentation:
procedure <name> is
new <generic name> <actuals>
function <name> is
new <generic name> <actuals>
package <name> is
new <generic name> <actuals>
|
type ... is
record
<component list>
case <discriminant name> is
when <choice> =>
<component list>
when <choice> =>
<component list>
end case;
end record;
|
Indentation for record alignment:
for <name> use
record <mod clause>
<component clause>
end record;
Tagged types and type extension:
type ... is tagged
record
<component list>
end record;
type ... is new ... with
record
<component list>
end record;
example
Default_String : constant String :=
"This is the long string returned by" &
" default. It is broken into multiple" &
" Ada source lines for convenience.";
...
if Input_Found then
Count_Characters;
else --not Input_Found
Reset_State;
Character_Total :=
First_Part_Total * First_Part_Scale_Factor +
Second_Part_Total * Second_Part_Scale_Factor +
Default_String'Length + Delimiter_Size;
end if;
end loop;
rationale
Indentation improves the readability of the code because it gives you a visual indicator of the program structure. The levels of nesting are clearly identified by indentation, and the first and last keywords in a construct can be matched visually.
While there is much discussion on the number of spaces to indent, the reason for indentation is code clarity. The fact that the code is indented consistently is more important than the number of spaces used for indentation.
Additionally, the Ada Reference Manual 1995, §1.1.4 [Annotated] states that the layout shown in the examples and syntax rules in the manual is the recommended code layout to be used for Ada programs: "The syntax rules describing structured constructs are presented in a form that corresponds to the recommended paragraphing . . . . Different lines are used for parts of a syntax rule if the corresponding parts of the construct described by the rule are intended to be on different lines . . . . It is recommended that all indentation be by multiples of a basic step of indentation (the number of spaces for the basic step is not defined)."
It is important to indent continuation lines differently from nested control structures to make them visually distinct. This prevents them from obscuring the structure of the code as you scan it.
Listing context clauses on individual lines allows easier maintenance; changing a context clause is less error-prone.
Indenting with spaces is more portable than indenting with tabs because tab characters are displayed differently by different terminals and printers.
exceptions
If you are using a variable width font, tabs will align better than spaces. However, depending on your tab setting, lines of successive indentation may leave you with a very short line length.
automation notes
The guidelines in this section are easily enforced with an automatic code formatter.
Alignment of Operators
guideline
- Align operators vertically to emphasize local program structure and semantics.
example
if Slot_A >= Slot_B then
Temporary := Slot_A;
Slot_A := Slot_B;
Slot_B := Temporary;
end if;
----------------------------------------------------------------
Numerator := B**2 - 4.0 * A * C;
Denominator := 2.0 * A;
Solution_1 := (B + Square_Root(Numerator)) / Denominator;
Solution_2 := (B - Square_Root(Numerator)) / Denominator;
----------------------------------------------------------------
X := A * B +
C * D +
E * F;
Y := (A * B + C) + (2.0 * D - E) - -- basic equation
3.5; -- account for error factor
rationale
Alignment makes it easier to see the position of the operators and, therefore, puts visual emphasis on what the code is doing.
The use of lines and spacing on long expressions can emphasize terms, precedence of operators, and other semantics. It can also leave room for highlighting comments within an expression.
exceptions
If vertical alignment of operators forces a statement to be broken over two lines, especially if the break is at an inappropriate spot, it may be preferable to relax the alignment guideline.
automation notes
The last example above shows a kind of "semantic alignment" that is not typically enforced or even preserved by automatic code formatters. If you break expressions into semantic parts and put each on a separate line, beware of using a code formatter later. It is likely to move the entire expression to a single line and accumulate all the comments at the end. However, there are some formatters that are intelligent enough to leave a line break intact when the line contains a comment. A good formatter will recognize that the last example above does not violate the guidelines and would, therefore, preserve it as written.
Alignment of Declarations
guideline
- Use vertical alignment to enhance the readability of declarations.
- Provide, at most, one declaration per line.
- Indent all declarations in a single declarative part at the same level.
instantiation
For declarations not separated by blank lines, follow these alignment rules:
- Align the colon delimiters.
- Align the := initialization delimiter.
- When trailing comments are used, align the comment delimiter.
- When the declaration overflows a line, break the line and add an indentation level for those lines that wrap. The preferred places to break, in order, are: (1) the comment delimiter; (2) the initialization delimiter; (3) the colon delimiter.
- For enumeration type declarations that do not fit on a single line, put each literal on a separate line, using the next level of indentation. When appropriate, semantically related literals can be arranged by row or column to form a table.
example
Variable and constant declarations can be laid out in a tabular format with columns separated by the symbols :, :=, and --
Prompt_Column : constant := 40;
Question_Mark : constant String := " ? "; -- prompt on error input
Prompt_String : constant String := " ==> ";
If this results in lines that are too long, they can be laid out with each part on a separate line with its unique indentation level:
subtype User_Response_Text_Frame is String (1 .. 72);
-- If the declaration needed a comment, it would fit here.
Input_Line_Buffer : User_Response_Text_Frame
:= Prompt_String &
String'(1 .. User_Response_Text_Frame'Length -
Prompt_String'Length => ' ');
Declarations of enumeration literals can be listed in one or more columns as:
type Op_Codes_In_Column is
(Push,
Pop,
Add,
Subtract,
Multiply,
Divide,
Subroutine_Call,
Subroutine_Return,
Branch,
Branch_On_Zero,
Branch_On_Negative);
or, to save space:
type Op_Codes_Multiple_Columns is
(Push, Pop, Add,
Subtract, Multiply, Divide,
Subroutine_Call, Subroutine_Return, Branch,
Branch_On_Zero, Branch_On_Negative);
or, to emphasize related groups of values:
type Op_Codes_In_Table is
(Push, Pop,
Add, Subtract, Multiply, Divide,
Subroutine_Call, Subroutine_Return,
Branch, Branch_On_Zero, Branch_On_Negative);
rationale
Many programming standards documents require tabular repetition of names, types, initial values, and meaning in unit header comments. These comments are redundant and can become inconsistent with the code. Aligning the declarations themselves in tabular fashion (see the examples above) provides identical information to both compiler and reader; enforces, at most, one declaration per line; and eases maintenance by providing space for initializations and necessary comments. A tabular layout enhances readability, thus preventing names from "hiding" in a mass of declarations. This applies to all declarations: types, subtypes, objects, exceptions, named numbers, and so forth.
automation notes
Most of the guidelines in this section are easily enforced with an automatic code formatter. The one exception is the last enumerated type example, which is laid out in rows based on the semantics of the enumeration literals. An automatic code formatter will not be able to do this and will likely move the enumeration literals to different lines. However, tools that are checking only for violations of the guidelines should accept the tabular form of an enumeration type declaration.
More on Alignment
guideline
- Align parameter modes and parentheses vertically.
instantiation
Specifically, it is recommended that you:
- Place one formal parameter specification per line.
- Vertically align parameter names, colons, the reserved word in, the reserved word out, and parameter subtypes.
- Place the first parameter specification on the same line as the subprogram or entry name. If any parameter subtypes are forced beyond the line length limit, place the first parameter specification on a new line indented the same as a continuation line.
example
procedure Display_Menu (Title : in String;
Options : in Menus;
Choice : out Alpha_Numerics);
The following two examples show alternate instantiations of this guideline:
procedure Display_Menu_On_Primary_Window
(Title : in String;
Options : in Menus;
Choice : out Alpha_Numerics);
or:
procedure Display_Menu_On_Screen (
Title : in String;
Options : in Menus;
Choice : out Alpha_Numerics
);
Aligning parentheses makes complicated relational expressions more clear:
if not (First_Character in Alpha_Numerics and then
Valid_Option(First_Character)) then
rationale
This alignment facilitates readability and understandability, and it is easy to achieve given automated support. Aligning parameter modes provides the effect of a table with columns for parameter name, mode, subtype, and, if necessary, parameter-specific comments. Vertical alignment of parameters across subprograms within a compilation unit increases the readability even more.
notes
Various options are available for subprogram layout. The second example above aligns all of the subprogram names and parameter names in a program. This has the disadvantage of occupying an unnecessary line where subprogram names are short and looking awkward if there is only one parameter.
The third example is a format commonly used to reduce the amount of editing required when parameter lines are added, deleted, or reordered. The parentheses do not have to be moved from line to line. However, the last parameter line is the only one without a semicolon.
exceptions
When an operator function has two or more formal parameters of the same type, it is more readable to declare the parameters in a single one-line list rather than to separate the formal parameter list into multiple formal parameter specifications.
type Color_Scheme is (Red, Purple, Blue, Green, Yellow, White, Black, Brown, Gray, Pink);
function "&" (Left, Right : Color_Scheme) return Color_Scheme;
automation notes
Most of the guidelines in this section are easily enforced with an automatic code formatter. The one exception is the last example, which shows vertical alignment of parentheses to emphasize terms of an expression. This is difficult to achieve with an automatic code formatter unless the relevant terms of the expression can be determined strictly through operator precedence.
Blank Lines
guideline
- Use blank lines to group logically related lines of text (NASA 1987).
example
if ... then
for ... loop
...
end loop;
end if;
This example separates different kinds of declarations with blank lines:
type Employee_Record is
record
Legal_Name : Name;
Date_Of_Birth : Date;
Date_Of_Hire : Date;
Salary : Money;
end record;
type Day is
(Monday, Tuesday, Wednesday, Thursday, Friday,
Saturday, Sunday);
subtype Weekday is Day range Monday .. Friday;
subtype Weekend is Day range Saturday .. Sunday;
rationale
When blank lines are used in a thoughtful and consistent manner, sections of related code are more visible to readers.
automation notes
Automatic formatters do not enforce this guideline well because the decision on where to insert blank lines is a semantic one. However, many formatters have the ability to leave existing blank lines intact. Thus, you can manually insert the lines and not lose the effect when you run such a formatter.
Pagination
guideline
- Highlight the top of each package or task specification, the top of each program unit body, and the end statement of each program unit.
instantiation
Specifically, it is recommended that you:
- Use file prologues, specification headers, and body headers to highlight those structures as recommended in Guideline 3.3.
- Use a line of dashes, beginning at the same column as the current indentation to highlight the definition of nested units embedded in a declarative part. Insert the line of dashes immediately before and immediately after the definition.
- If two dashed lines are adjacent, omit the longer of the two.
example
with Basic_Types;
package body SPC_Numeric_Types is
---------------------------------------------------------------------
function Max
(Left : in Basic_Types.Tiny_Integer;
Right : in Basic_Types.Tiny_Integer)
return Basic_Types.Tiny_Integer is
begin
if Right < Left then
return Left;
else
return Right;
end if;
end Max;
---------------------------------------------------------------------
function Min
(Left : in Basic_Types.Tiny_Integer;
Right : in Basic_Types.Tiny_Integer)
return Basic_Types.Tiny_Integer is
begin
if Left < Right then
return Left;
else
return Right;
end if;
end Min;
---------------------------------------------------------------------
use Basic_Types;
begin -- SPC_Numeric_Types
Max_Tiny_Integer := Min(System_Max, Local_Max);
Min_Tiny_Integer := Max(System_Min, Local_Min);
-- ...
end SPC_Numeric_Types;
rationale
It is easy to overlook parts of program units that are not visible on the current page or screen. The page lengths of presentation hardware and software vary widely. By clearly marking the program's logical page boundaries (e.g., with a dashed line), you enable a reader to quickly check whether all of a program unit is visible. Such pagination also makes it easier to scan a large file quickly, looking for a particular program unit.
exceptions
This guideline does not address code layout on the physical "page" because the dimensions of such pages vary widely and no single guideline is appropriate.
automation notes
The guidelines in this section are easily enforced with an automatic code formatter.
Number of Statements Per Line
guideline
- Start each statement on a new line.
- Write no more than one simple statement per line.
- Break compound statements over multiple lines.
example
Use:
if End_Of_File then
Close_File;
else
Get_Next_Record;
end if;
rather than:
if End_Of_File then Close_File; else Get_Next_Record; end if;
exceptional case:
Put("A="); Natural_IO.Put(A); New_Line;
Put("B="); Natural_IO.Put(B); New_Line;
Put("C="); Natural_IO.Put(C); New_Line;
rationale
A single statement on each line enhances the reader's ability to find statements and helps prevent statements being missed. Similarly, the structure of a compound statement is clearer when its parts are on separate lines.
exceptions
If a statement is longer than the remaining space on the line, continue it on the next line. This guideline includes declarations, context clauses, and subprogram parameters.
According to the Ada Reference Manual 1995, §1.1.4 [Annotated], "The preferred places for other line breaks are after semicolons."
automation notes
The guidelines in this section are easily enforced with an automatic code formatter, with the single exception of the last example, which shows a semantic grouping of multiple statements onto a single line.
exceptions
The example of Put and New_Line statements shows a legitimate exception. This grouping of closely related statements on the same line makes the structural relationship between the groups clear.
Source Code Line Length
guideline
- Adhere to a maximum line length limit for source code (Nissen and Wallis 1984, §2.3).
instantiation
Specifically, it is recommended that you:
- Limit source code line lengths to a maximum of 72 characters.
rationale
When Ada code is ported from one system to another, there may be restrictions on the record size of source line statements, possibly for one of the following reasons: some operating systems may not support variable length records for tape I/O, or some printers and terminals support an 80-character line width with no line-wrap. See further rationale in the note for Guideline 7.1.2.
Source code must sometimes be published for various reasons, and letter-size paper is not as forgiving as a computer listing in terms of the number of usable columns.
In addition, there are human limitations in the width of the field of view for understanding at the level required for reading source code. These limitations correspond roughly to the 70- to 80-column range.
exceptions
An alternate instantiation is to limit source code length to 79 characters. The 79-character limit differentiates the code from the FORTRAN 72-character limit. It also avoids problems with 80-character width terminals where the character in the last column may not print correctly.
automation notes
The guidelines in this section are easily enforced with an automatic code formatter.
Summary
code formatting
- Use consistent spacing around delimiters.
- Use the same spacing as you would in regular prose.
- Indent and align nested control structures, continuation lines, and embedded units consistently.
- Distinguish between indentation for nested control structures and for continuation lines.
- Use spaces for indentation, not the tab character (Nissen and Wallis 1984, §2.2).
- Align operators vertically to emphasize local program structure and semantics.
- Use vertical alignment to enhance the readability of declarations.
- Provide, at most, one declaration per line.
- Indent all declarations in a single declarative part at the same level.
- Align parameter modes and parentheses vertically.
- Use blank lines to group logically related lines of text (NASA 1987).
- Highlight the top of each package or task specification, the top of each program unit body, and the end statement of each program unit.
- Start each statement on a new line.
- Write no more than one simple statement per line.
- Break compound statements over multiple lines.
- Adhere to a maximum line length limit for source code (Nissen and Wallis 1984, §2.3).
Readability
Introduction
This chapter recommends ways of using Ada features to make reading and understanding code easier. There are many myths about comments and readability. The responsibility for true readability rests more with naming and code structure than with comments. Having as many comment lines as code lines does not imply readability; it more likely indicates the writer does not understand what is important to communicate.
Spelling
Spelling conventions in source code include rules for capitalization and use of underscores, numbers, and abbreviations. If you follow these conventions consistently, the resulting code is clearer and more readable.
Use of Underscores
guideline
- Use underscores to separate words in a compound name.
example
Miles_Per_Hour
Entry_Value
rationale
When an identifier consists of more than one word, it is much easier to read if the words are separated by underscores. Indeed, there is precedent in English in which compound words are separated by a hyphen or a space. In addition to promoting readability of the code, if underscores are used in names, a code formatter has more control over altering capitalization. See Guideline 3.1.3.
Numbers
guideline
- Represent numbers in a consistent fashion.
- Represent literals in a radix appropriate to the problem.
- Use underscores to separate digits the same way commas or periods (or spaces for nondecimal bases) would be used in normal text.
- When using scientific notation, make the E consistently either uppercase or lowercase.
- In an alternate base, represent the alphabetic characters in either all uppercase or all lowercase.
instantiation
- Decimal and octal numbers are grouped by threes beginning on the left side of the radix point and by fives beginning on the right side of the radix point.
- The E is always capitalized in scientific notation.
- Use uppercase for the alphabetic characters representing digits in bases above 10.
- Hexadecimal numbers are grouped by fours beginning on either side of the radix point.
example
type Maximum_Samples is range 1 .. 1_000_000;
type Legal_Hex_Address is range 16#0000# .. 16#FFFF#;
type Legal_Octal_Address is range 8#000_000# .. 8#777_777#;
Avogadro_Number : constant := 6.02216_9E+23;
To represent the number 1/3 as a constant, use:
One_Third : constant := 1.0 / 3.0;
Avoid this use:
One_Third_As_Decimal_Approximation : constant := 0.33333_33333_3333;
or:
One_Third_Base_3 : constant := 3#0.1#;
rationale
Consistent use of uppercase or lowercase aids scanning for numbers. Underscores serve to group portions of numbers into familiar patterns. Consistency with common use in everyday contexts is a large part of readability.
notes
If a rational fraction is represented in a base in which it has a terminating rather than a repeating representation, as 3#0.1# does in the example above, it may have increased accuracy upon conversion to the machine base. (This is wrong for named numbers as in this example - they must be calculated exactly.)
Capitalization
guideline
- Make reserved words and other elements of the program visually distinct from each other.
instantiation
- Use lowercase for all reserved words (when used as reserved words).
- Use mixed case for all other identifiers, a capital letter beginning every word separated by underscores.
- Use uppercase for abbreviations and acronyms (see automation notes).
example
...
type Second_Of_Day is range 0 .. 86_400;
type Noon_Relative_Time is (Before_Noon, After_Noon, High_Noon);
subtype Morning is Second_Of_Day range 0 .. 86_400 / 2 - 1;
subtype Afternoon is Second_Of_Day range Morning'Last + 2 .. 86_400;
...
Current_Time := Second_Of_Day(Calendar.Seconds(Calendar.Clock));
if Current_Time in Morning then
Time_Of_Day := Before_Noon;
elsif Current_Time in Afternoon then
Time_Of_Day := After_Noon;
else
Time_Of_Day := High_Noon;
end if;
case Time_Of_Day is
when Before_Noon => Get_Ready_For_Lunch;
when High_Noon => Eat_Lunch;
when After_Noon => Get_To_Work;
end case;
...
rationale
Visually distinguishing reserved words allows you to focus on program structure alone, if desired, and also aids scanning for particular identifiers.
The instantiation chosen here is meant to be more readable for the experienced Ada programmer, who does not need reserved words to leap off the page. Beginners to any language often find that reserved words should be emphasized to help them find the control structures more easily. Because of this, instructors in the classroom and books introducing the Ada language may want to consider an alternative instantiation. The Ada Reference Manual (1995) chose bold lowercase for all reserved words.
automation notes
Ada names are not case sensitive. Therefore, the names max_limit
, MAX_LIMIT
, and Max_Limit
denote the same object or entity. A good code formatter should be able to automatically convert from one style to another as long as the words are delimited by underscores.
As recommended in Guideline 3.1.4, abbreviations should be project-wide. An automated tool should allow a project to specify those abbreviations and format them accordingly.
Abbreviations
guideline
- Do not use an abbreviation of a long word as an identifier where a shorter synonym exists.
- Use a consistent abbreviation strategy.
- Do not use ambiguous abbreviations.
- To justify its use, an abbreviation must save many characters over the full word.
- Use abbreviations that are well-accepted in the application domain.
- Maintain a list of accepted abbreviations, and use only abbreviations on that list.
example
Use:
Time_Of_Receipt
rather than:
Recd_Time or R_Time
But in an application that commonly deals with message formats that meet military standards, DOD_STD_MSG_FMT
is an acceptable abbreviation for:
Department_Of_Defense_Standard_Message_Format.
rationale
Many abbreviations are ambiguous or unintelligible unless taken in context. As an example, Temp could indicate either temporary or temperature. For this reason, you should choose abbreviations carefully when you use them. The rationale in Guideline 8.1.2 provides a more thorough discussion of how context should influence the use of abbreviations.
Because very long variable names can obscure the structure of the program, especially in deeply nested (indented) control structures, it is a good idea to try to keep identifiers short and meaningful. Use short unabbreviated names whenever possible. If there is no short word that will serve as an identifier, then a well-known unambiguous abbreviation is the next best choice, especially if it comes from a list of standard abbreviations used throughout the project.
You can establish an abbreviated format for a fully qualified name using the renames clause. This capability is useful when a very long, fully qualified name would otherwise occur many times in a localized section of code (see Guideline 5.7.2).
A list of accepted abbreviations for a project provides a standard context for using each abbreviation.
Naming Conventions
Choose names that clarify the object's or entity's intended use. Ada allows identifiers to be any length as long as the identifier fits on a line with all characters being significant (including underscores). Identifiers are the names used for variables, constants, program units, and other entities within a program.
Names
guideline
- Choose names that are as self-documenting as possible.
- Use a short synonym instead of an abbreviation (see Guideline 3.1.4).
- Use names given by the application, but do not use obscure jargon.
- Avoid using the same name to declare different kinds of identifiers.
example
In a tree-walker, using the name Left
instead of Left_Branch
is sufficient to convey the full meaning given the context. However, use Time_Of_Day
instead of TOD
.
Mathematical formulas are often given using single-letter names for variables. Continue this convention for mathematical equations where they would recall the formula, for example:
A*(X**2) + B*X + C.
With the use of child packages, a poor choice of package, subunit, and identifier names can lead to a visibility clash with subunits. See the Rationale (1995, §8.1) for a sample of the resulting, rather obscure code.
rationale
A program that follows these guidelines can be more easily comprehended. Self-documenting names require fewer explanatory comments. Empirical studies have shown that you can further improve comprehension if your variable names are not excessively long (Schneiderman 1986, 7). The context and application can help greatly. The unit of measure for numeric entities can be a source of subtype names.
You should try not to use the same name as an identifier for different declarations, such as an object and a child package. Overusing an identifier in seemingly different name spaces can, in fact, lead to visibility clashes if the enclosing program units are intended to work together.
notes
See Guideline 8.1.2 for a discussion on how to use the application domain as a guideline for selecting abbreviations.
Subtype Names
guideline
- Use singular, general nouns as subtype identifiers.
- Choose identifiers that describe one of the subtype's values.
- Consider using suffixes for subtype identifiers that define visible access types, visible subranges, or visible array types.
- For private types, do not use identifier constructions (e.g., suffixes) that are unique to subtype identifiers.
- Do not use the subtype names from predefined packages.
example
type Day is
(Monday, Tuesday, Wednesday, Thursday, Friday,
Saturday, Sunday);
type Day_Of_Month is range 0 .. 31;
type Month_Number is range 1 .. 12;
type Historical_Year is range -6_000 .. 2_500;
type Date is
record
Day : Day_Of_Month;
Month : Month_Number;
Year : Historical_Year;
end record;
In particular, Day
should be used in preference to Days
or Day_Type
.
The identifier Historical_Year
might appear to be specific, but it is actually general, with the adjective historical
describing the range constraint:
------------------------------------------------------------------------
procedure Disk_Driver is
-- In this procedure, a number of important disk parameters are
-- linked.
Number_Of_Sectors : constant := 4;
Number_Of_Tracks : constant := 200;
Number_Of_Surfaces : constant := 18;
Sector_Capacity : constant := 4_096;
Track_Capacity : constant := Number_Of_Sectors * Sector_Capacity;
Surface_Capacity : constant := Number_Of_Tracks * Track_Capacity;
Disk_Capacity : constant := Number_Of_Surfaces * Surface_Capacity;
type Sector_Range is range 1 .. Number_Of_Sectors;
type Track_Range is range 1 .. Number_Of_Tracks;
type Surface_Range is range 1 .. Number_Of_Surfaces;
type Track_Map is array (Sector_Range) of ...;
type Surface_Map is array (Track_Range) of Track_Map;
type Disk_Map is array (Surface_Range) of Surface_Map;
begin -- Disk_Driver
...
end Disk_Driver;
------------------------------------------------------------------------
The suffixes _Capacity
, _Range
, and _Map
help define the purpose of the above subtypes and avoid the search for synonyms for the sector, track, and surface abstractions. Without the suffixes, you would need three different names per abstraction, one to describe each of the concepts succinctly named in the suffix. This recommendation only applies to certain visible subtypes. Private types, for example, should be given a good name that reflects the abstraction being represented.
rationale
When this style and the suggested style for object identifiers are used, program code more closely resembles English (see Guideline 3.2.3). Furthermore, this style is consistent with the names of the language's predefined identifiers. They are not named Integers
, Booleans
, Integer_Type
, or Boolean_Type
.
However, using the name of a subtype from the predefined packages is sure to confuse a programmer when that subtype appears somewhere without a package qualification.
notes
This style guide tries to be consistent with the Ada Reference Manual (1995) in use of the terms "type" and "subtype" name. In general, a "type" refers to the abstract concept, as in a type declaration, while the "subtype" refers to the name given to that abstract concept in an actual declaration. Thus, what was called a type name in Ada 83 (Ada Reference Manual 1983) is now called a subtype name.
Object Names
guideline
- Use predicate clauses or adjectives for Boolean objects.
- Use singular, specific nouns as object identifiers.
- Choose identifiers that describe the object's value during execution.
- Use singular, general nouns as identifiers for record components.
example
Non-Boolean objects:
Today : Day;
Yesterday : Day;
Retirement_Date : Date;
Boolean objects:
User_Is_Available : Boolean; -- predicate clause
List_Is_Empty : Boolean; -- predicate clause
Empty : Boolean; -- adjective
Bright : Boolean; -- adjective
rationale
Using specific nouns for objects establishes a context for understanding the object's value, which is one of the general values described by the subtype's name (see Guideline 3.2.2). Object declarations become very English-like with this style. For example, the first declaration above is read as "Today is a Day."
General nouns, rather than specific, are used for record components because a record object's name will supply the context for understanding the component. Thus, the following component is understood as "the year of retirement":
Retirement_Date.Year
Following conventions that relate object types and parts of speech makes code read more like text. For example, because of the names chosen, the following code segment needs no comments:
if List_Is_Empty then
Number_Of_Elements := 0;
else
Number_Of_Elements := Length_Of_List;
end if;
notes
If it is difficult to find a specific noun that describes an object's value during the entire execution of a program, the object is probably serving multiple purposes. Multiple objects should be used in such a case.
Naming of Tagged Types and Associated Packages
guideline
- Use a consistent naming convention for tagged types and associated packages.
instantiation
Naming conventions spark "religious wars"; therefore, two different instantiations are presented. The first instantiation integrates the use of object-oriented features. Except for two special cases, it applies the same naming conventions to declarations, independent of whether they use an object-oriented feature:
- Name tagged types no differently than subtype names (see Guideline 3.2.2).
- Use the prefix
Abstract_
for packages that export an abstraction for which you intend to provide multiple implementations (see Guideline 9.2.4). - Use the suffix
_Mixin
for packages that provide units of functionality that can be "mixed in" to core abstractions.
The second instantiation highlights the use of object-oriented features through special names or suffixes:
- Name class packages after the object they represent, without a suffix (Rosen 1995).
- Name mixin packages after the facet they represent, appending the suffix _Facet (Rosen 1995).
- Name the main tagged type
Instance
(Rosen 1995). - Follow the declaration of the specific type with a subtype named
Class
for the corresponding class-wide type (Rosen 1995).
example
The following two-part example from the Rationale (1995, §§4.4.4 and 4.6.2) applies the naming conventions of the first instantiation.
For the first part of this example, assume the type Set_Element
was declared elsewhere:
package Abstract_Sets is
type Set is abstract tagged private;
-- empty set
function Empty return Set is abstract;
-- build set with 1 element
function Unit (Element: Set_Element) return Set is abstract;
-- union of two sets
function Union (Left, Right: Set) return Set is abstract;
-- intersection of two sets
function Intersection (Left, Right: Set) return Set is abstract;
-- remove an element from a set
procedure Take (From : in out Set;
Element : out set_Element) is abstract;
Element_Too_Large : exception;
private
type Set is abstract tagged null record;
end Abstract_Sets;
with Abstract_Sets;
package Bit_Vector_Sets is -- one implementation of set abstraction
type Bit_Set is new Abstract_Sets.Set with private;
...
private
Bit_Set_Size : constant := 64;
type Bit_Vector is ...
type Bit_Set is new Abstract_Sets.Set with
record
Data : Bit_Vector;
end record;
end Bit_Vector_Sets;
with Abstract_Sets;
package Sparse_Sets -- alternate implementation of set abstraction
type Sparse_Set is new Abstract_Sets.Set with private;
...
private
...
end Bit_Vector_Sets;
The second part of this example applies the naming convention to mixin packages that support a windowing system:
-- assume you have type Basic_Window is tagged limited private;
generic
type Some_Window is abstract new Basic_Window with private;
package Label_Mixin is
type Window_With_Label is abstract new Some_Window with private;
...
private
...
end Label_Mixin;
generic
type Some_Window is abstract new Basic_Window with private;
package Border_Mixin is
type Window_With_Label is abstract new Some_Window with private;
...
private
...
end Border_Mixin;
The following example applies the naming conventions of the second instantiation, as discussed in Rosen (1995):
package Shape is
subtype Side_Count is range 0 .. 100;
type Instance (Sides: Side_Count) is tagged private;
subtype Class is Instance'Class;
. . .
-- operations on Shape.Instance
private
. . .
end Shape;
with Shape; use Shape;
package Line is
type Instance is new Shape.Instance with private;
subtype Class is Instance'Class;
. . .
-- Overridden or new operations
private
. . .
end Line;
with Shape; use Shape;
generic
type Origin is new Shape.Instance;
package With_Color_Facet is
type Instance is new Origin with private;
subtype Class is Instance'Class;
-- operations for colored shapes
private
. . .
end With_Color_Facet;
with Line; use Line;
with With_Color_Facet;
package Colored_Line is new With_Color_Facet (Line.Instance);
Sample declarations might look like:
Red_Line : Colored_Line.Instance;
procedure Draw (What : Shape.Instance);
The above scheme works whether you use full names or a use
clause. As long as you use the same name for all the specific types (i.e., type Instance
) and class-wide types, the unqualified names will always hide one another. Thus, the compiler will insist you use full name qualification to resolve the ambiguity introduced by the use clause (Rosen 1995).
rationale
You want to use a naming scheme that is consistent and readable and conveys the intent of the abstraction. Ideally, the naming scheme should be uniform in how it handles the different ways in which tagged types are used to create classes. If the naming convention is too rigid, however, you will write code fragments that appear stilted from a readability point of view. By using a similar naming convention for type extension through derivation and through generic mixin (see also Guideline 9.5.1), you achieve readable declarations of objects and procedures.
notes
A naming convention for classes draws a hard line between object-oriented abstractions and other kinds of abstractions. Given that engineers have been defining abstract data types in Ada 83 (Ada Reference Manual 1983) for over 10 years, you may not want to change the naming convention just for the sake of using type extension with a type. You must consider how important it is to call out uses of inheritance in the overall use of abstractions in your program. If you prefer to emphasize abstraction, in general, over the mechanism used to implement the abstraction (i.e., inheritance, type-extension, and polymorphism), you may not want to impose such a stringent naming convention. You do not hamper quality by favoring a smoother transition in naming conventions from abstractions developed without inheritance to those developed with inheritance.
If you choose a naming convention that highlights the use of object-oriented features and later decide to change the declaration to one that does not use an object-oriented feature, the change may be expensive. You must naturally change all occurrences of the names and must be careful not to introduce errors as you update the names. If you choose a naming convention that prohibits the use of suffixes or prefixes to characterize the declaration, you lose the opportunity to convey the intended usage of the declared item.
Program Unit Names
guideline
- Use action verbs for procedures and entries.
- Use predicate clauses for Boolean functions.
- Use nouns for non-Boolean functions.
- Give packages names that imply a higher level of organization than subprograms. Generally, these are noun phrases that describe the abstraction provided.
- Give tasks names that imply an active entity.
- Use nouns descriptive of the data being protected for protected units.
- Consider naming generic subprograms as if they were nongeneric subprograms.
- Consider naming generic packages as if they were nongeneric packages.
- Make the generic names more general than the instantiated names.
example
The following are sample names for elements that compose an Ada program:
Sample procedure names:
procedure Get_Next_Token -- get is a transitive verb
procedure Create -- create is a transitive verb
Sample function names for Boolean-valued functions:
function Is_Last_Item -- predicate clause
function Is_Empty -- predicate clause
Sample function names for non-Boolean-valued functions:
function Successor -- common noun
function Length -- attribute
function Top -- component
Sample package names:
package Terminals is -- common noun
package Text_Routines is -- common noun
Sample protected objects:
protected Current_Location is -- data being protected
protected type Guardian is -- noun implying protection
Sample task names:
task Terminal_Resource_Manager is -- common noun that shows action
The following sample piece of code shows the clarity that results from using the parts-of-speech naming conventions:
Get_Next_Token(Current_Token);
case Current_Token is
when Identifier => Process_Identifier;
when Numeric => Process_Numeric;
end case; -- Current_Token
if Is_Empty(Current_List) then
Number_Of_Elements := 0;
else
Number_Of_Elements := Length(Current_List);
end if;
When packages and their subprograms are named together, the resulting code is very descriptive:
if Stack.Is_Empty(Current_List) then
Current_Token := Stack.Top(Current_List);
end if;
rationale
Using these naming conventions creates understandable code that reads much like natural language. When verbs are used for actions, such as subprograms, and nouns are used for objects, such as the data that the subprogram manipulates, code is easier to read and understand. This models a medium of communication already familiar to a reader. Where the pieces of a program model a real-life situation, using these conventions reduces the number of translation steps involved in reading and understanding the program. In a sense, your choice of names reflects the level of abstraction from computer hardware toward application requirements.
See also Guideline 3.2.4 for the use of special-purpose suffixes in packages associated with tagged types.
notes
There are some conflicting conventions in current use for task entries. Some programmers and designers advocate naming task entries with the same conventions used for subprograms to blur the fact that a task is involved. Their reasoning is that if the task is reimplemented as a package, or vice versa, the names need not change. Others prefer to make the fact of a task entry as explicit as possible to ensure that the existence of a task with its presumed overhead is recognizable. Project-specific priorities may be useful in choosing between these conventions.
Constants and Named Numbers
guideline
- Use symbolic values instead of literals where the symbolic value improves readability.
- Use symbolic values instead of literals if the value occurs at more than one place and might need to be changed.
- Use the predefined constants Ada.Numerics.Pi and Ada.Numerics.e for the mathematical constants Pi and e.
- Use constants instead of variables for constant values.
- Use a constant when the value is specific to a type or when the value must be static.
- Use named numbers instead of constants, whenever possible.
- Use named numbers to replace numeric literals whose type or context is truly universal.
- Use constants for objects whose values cannot change after elaboration (United Technologies 1987).
- Show relationships between symbolic values by defining them with static expressions.
- Use linearly independent sets of literals.
- Use attributes like 'First and 'Last instead of literals, wherever possible.
example
3.14159_26535_89793 -- literal
Max_Entries : constant Integer := 400; -- constant
Avogadros_Number : constant := 6.022137 * 10**23; -- named number
Avogadros_Number / 2 -- static expression
Avogadros_Number -- symbolic value
Declaring Pi
as a named number (assuming a with
clause for the predefined package Ada.Numerics
in the Ada Reference Manual 1995, §A.5 [Annotated] allows it to be referenced symbolically in the assignment statement below:
Area := Pi * Radius**2; -- if radius is known.
instead of:
Area := 3.14159 * Radius**2; -- Needs explanatory comment
Also, Ada.Characters.Latin_1.Bel
is more expressive than Character'Val(8#007#)
.
Clarity of constant and named number declarations can be improved by using other constant and named numbers. For example:
Bytes_Per_Page : constant := 512;
Pages_Per_Buffer : constant := 10;
Buffer_Size : constant := Pages_Per_Buffer * Bytes_Per_Page;
is more self-explanatory and easier to maintain than:
Buffer_Size : constant := 5_120; -- ten pages
The following literals should be constants:
if New_Character = '$' then -- "constant" that may change
...
if Current_Column = 7 then -- "constant" that may change
rationale
Using identifiers instead of literals makes the purpose of expressions clear, reducing the need for comments. Constant declarations consisting of expressions of numeric literals are safer because they do not need to be computed by hand. They are also more enlightening than a single numeric literal because there is more opportunity for embedding explanatory names. Clarity of constant declarations can be improved further by using other related constants in static expressions defining new constants. This is not less efficient because static expressions of named numbers are computed at compile time.
A constant has a type. A named number can only be a universal type: universal_integer or universal_real. Strong typing is enforced for constants but not for named numbers or literals. Named numbers allow compilers to generate more efficient code than for constants and to perform more complete error checking at compile time. If the literal contains a large number of digits (as Pi in the example above), the use of an identifier reduces keystroke errors. If keystroke errors occur, they are easier to locate either by inspection or at compile time.
Independence of literals means that the few literals that are used do not depend on one another and that any relationship between constant or named values is shown in the static expressions. Linear independence of literal values gives the property that if one literal value changes, all of the named numbers of values dependent on that literal are automatically changed.
See Guideline 4.1.4 for additional guidelines on choosing a parameterless function versus a constant.
notes
There are situations where a literal is a better choice than a name. For this to be the case, the following conditions have to be fulfilled:
- The literal has to be self-explanatory in the respective context, such that replacing the literal by a symbolic value would not improve the readability.
- The value is either unchangeable or only occurs at one single place in the code, such that replacing the literal by a symbolic value would not improve the maintainability.
For example, the literals in the following well known relationship are both self-explanatory and unchangeable:
Fahrenheit := 32.0 + (9.0 * Celsius) / 5.0;
As a second example, dividing by the literal 2 is self-explanatory in a binary search algorithm context. And, since the value is also unchangeably related with the algorithm, it also does not matter if the literal occurs at more than one place in the code (for example due to loop unrolling). Therefore, the use of a symbolic value like the following would neither improve readability nor maintainability:
Binary_Search_Divisor : constant := 2;
Exceptions
guideline
- Use a name that indicates the kind of problem the exception represents.
example
Invalid_Name: exception;
Stack_Overflow: exception;
rationale
Naming exceptions according to the kind of problem they are detecting enhances the readability of the code. You should name your exceptions as precisely as you can so that the maintainer of the code understands why the exception might be raised. A well-named exception should be meaningful to the clients of the package declaring the exception.
Constructors
guideline
- Include a prefix like
New
,Make
, orCreate
in naming constructors (in this sense, operations to create and/or initialize an object). - Use names indicative of their content for child packages containing constructors.
instantiation
- Name a child package containing constructors
<whatever>.Constructor
.
example
function Make_Square (Center : Cartesian_Coordinates;
Side : Positive)
return Square;
rationale
Including a word like New
, Make
, or Create
in a constructor name makes its purpose clear. You may want to restrict the use of the prefix New to constructors that return an access value because the prefix suggests the internal use of an allocator.
Putting all constructors in a child package, even when they return access values, is a useful organizational principle.
For information regarding the use of Ada constructors, refer to Guideline 9.3.3.
Comments
Comments in source text are a controversial issue. There are arguments both for and against the view that comments enhance readability. In practice, the biggest problem with comments is that people often fail to update them when the associated source text is changed, thereby making the commentary misleading. Commentary should be reserved for expressing needed information that cannot be expressed in code and highlighting cases where there are overriding reasons to violate one of the guidelines. If possible, source text should use self-explanatory names for objects and program units, and it should use simple, understandable program structures so that little additional commentary is needed. The extra effort in selecting (and entering) appropriate names and the extra thought needed to design clean and understandable program structures are fully justified.
Use comments to state the intent of the code. Comments that provide an overview of the code help the maintenance programmer see the forest for the trees. The code itself is the detailed "how" and should not be paraphrased in the comments.
Comments should be minimized. They should provide needed information that cannot be expressed in the Ada language, emphasize the structure of code, and draw attention to deliberate and necessary violations of the guidelines. Comments are present either to draw attention to the real issue being exemplified or to compensate for incompleteness in the sample program.
Maintenance programmers need to know the causal interaction of noncontiguous pieces of code to get a global, more or less complete sense of the program. They typically acquire this kind of information from mental simulation of parts of the code. Comments should be sufficient enough to support this process (Soloway et al. 1986).
This section presents general guidelines about how to write good comments. It then defines several different classes of comments with guidelines for the use of each. The classes are file headers, program unit specification headers, program unit body headers, data comments, statement comments, and marker comments.
General Comments
guideline
- Make the code as clear as possible to reduce the need for comments.
- Never repeat information in a comment that is readily available in the code.
- Where a comment is required, make it concise and complete.
- Use proper grammar and spelling in comments.
- Make comments visually distinct from the code.
- Structure comments in header so that information can be automatically extracted by a tool.
rationale
The structure and function of well-written code is clear without comments. Obscured or badly structured code is hard to understand, maintain, or reuse regardless of comments. Bad code should be improved, not explained. Reading the code itself is the only way to be absolutely positive about what the code does; therefore, the code should be made as readable as possible.
Using comments to duplicate information in the code is a bad idea for several reasons. First, it is unnecessary work that decreases productivity. Second, it is very difficult to correctly maintain the duplication as the code is modified. When changes are made to existing code, it is compiled and tested to make sure that it is once again correct. However, there is no automatic mechanism to make sure that the comments are correctly updated to reflect the changes. Very often, the duplicate information in a comment becomes obsolete at the first code change and remains so through the life of the software. Third, when comments about an entire system are written from the limited point of view of the author of a single subsystem, the comments are often incorrect from the start.
Comments are necessary to reveal information difficult or impossible to obtain from the code. Subsequent chapters of this book contain examples of such comments. Completely and concisely present the required information.
The purpose of comments is to help readers understand the code. Misspelled, ungrammatical, ambiguous, or incomplete comments defeat this purpose. If a comment is worth adding, it is worth adding correctly in order to increase its usefulness.
Making comments visually distinct from the code by indenting them, grouping them together into headers, or highlighting them with dashed lines is useful because it makes the code easier to read. Subsequent chapters of this book elaborate on this point.
automation notes
The guideline about storing redundant information in comments applies only to manually generated comments. There are tools that automatically maintain information about the code (e.g., calling units, called units, cross-reference information, revision histories, etc.), storing it in comments in the same file as the code. Other tools read comments but do not update them, using the information from the comments to automatically generate detailed design documents and other reports.
The use of such tools is encouraged and may require that you structure your header comments so they can be automatically extracted and/or updated. Beware that tools that modify the comments in a file are only useful if they are executed frequently enough. Automatically generated obsolete information is even more dangerous than manually generated obsolete information because it is more trusted by the reader.
Revision histories are maintained much more accurately and completely by configuration management tools. With no tool support, it is very common for an engineer to make a change and forget to update the revision history. If your configuration management tool is capable of maintaining revision histories as comments in the source file, then take advantage of that capability, regardless of any compromise you might have to make about the format or location of the revision history. It is better to have a complete revision history appended to the end of the file than to have a partial one formatted nicely and embedded in the file header.
File Headers
guideline
- Put a file header on each source file.
- Place ownership, responsibility, and history information for the file in the file header.
instantiation
- Put a copyright notice in the file header.
- Put the author's name and department in the file header.
- Put a revision history in the file header, including a summary of each change, the date, and the name of the person making the change.
example
------------------------------------------------------------------------
-- Copyright (c) 1991, Software Productivity Consortium, Inc.
-- All rights reserved.
--
-- Author: J. Smith
-- Department:System Software Department
--
-- Revision History:
-- 7/9/91 J. Smith
-- - Added function Size_Of to support queries of node sizes.
-- - Fixed bug in Set_Size which caused overlap of large nodes.
-- 7/1/91 M. Jones
-- - Optimized clipping algorithm for speed.
-- 6/25/91 J. Smith
-- - Original version.
------------------------------------------------------------------------
rationale
Ownership information should be present in each file if you want to be sure to protect your rights to the software. Furthermore, for high visibility, it should be the first thing in the file.
Responsibility and revision history information should be present in each file for the sake of future maintainers; this is the header information most trusted by maintainers because it accumulates. It does not evolve. There is no need to ever go back and modify the author's name or the revision history of a file. As the code evolves, the revision history should be updated to reflect each change. At worst, it will be incomplete; it should rarely be wrong. Also, the number and frequency of changes and the number of different people who made the changes over the history of a unit can be good indicators of the integrity of the implementation with respect to the design.
Information about how to find the original author should be included in the file header, in addition to the author's name, to make it easier for maintainers to find the author in case questions arise. However, detailed information like phone numbers, mail stops, office numbers, and computer account user names are too volatile to be very useful. It is better to record the department for which the author was working when the code was written. This information is still useful if the author moves offices, changes departments, or even leaves the company because the department is likely to retain responsibility for the original version of the code.
notes
With modern configuration management systems, explicitly capturing version history as header comments may be superfluous. The configuration management tool maintains a more reliable and consistent (from a content point of view) change history. Some systems can re-create earlier versions of a unit.
Program Unit Specification Headers
guideline
- Put a header on the specification of each program unit.
- Place information required by the user of the program unit in the specification header.
- Do not repeat information (except unit name) in the specification header that is present in the specification.
- Explain what the unit does, not how or why it does it.
- Describe the complete interface to the program unit, including any exceptions it can raise and any global effects it can have.
- Do not include information about how the unit fits into the enclosing software system.
- Describe the performance (time and space) characteristics of the unit.
instantiation
- Put the name of the program unit in the header.
- Briefly explain the purpose of the program unit.
- For packages, describe the effects of the visible subprograms on each other and how they should be used together.
- List all exceptions that can be raised by the unit.
- List all global effects of the unit.
- List preconditions and postconditions of the unit.
- List hidden tasks activated by the unit.
- Do not list the names of parameters of a subprogram.
- Do not list the names of package subprograms just to list them.
- Do not list the names of all other units used by the unit.
- Do not list the names of all other units that use the unit.
example
------------------------------------------------------------------------
-- AUTOLAYOUT
--
-- Purpose:
-- This package computes positional information for nodes and arcs
-- of a directed graph. It encapsulates a layout algorithm which is
-- designed to minimize the number of crossing arcs and to emphasize
-- the primary direction of arc flow through the graph.
--
-- Effects:
-- - The expected usage is:
-- 1. Call Define for each node and arc to define the graph.
-- 2. Call Layout to assign positions to all nodes and arcs.
-- 3. Call Position_Of for each node and arc to determine the
-- assigned coordinate positions.
-- - Layout can be called multiple times, and recomputes the
-- positions of all currently defined nodes and arcs each time.
-- - Once a node or arc has been defined, it remains defined until
-- Clear is called to delete all nodes and arcs.
--
-- Performance:
-- This package has been optimized for time, in preference to space.
-- Layout times are on the order of N*log(N) where N is the number
-- of nodes, but memory space is used inefficiently.
------------------------------------------------------------------------
package Autolayout is
...
---------------------------------------------------------------------
-- Define
--
-- Purpose:
-- This procedure defines one node of the current graph.
-- Exceptions:
-- Node_Already_Defined
---------------------------------------------------------------------
procedure Define
(New_Node : in Node);
---------------------------------------------------------------------
-- Layout
--
-- Purpose:
-- This procedure assigns coordinate positions to all defined
-- nodes and arcs.
-- Exceptions:
-- None.
---------------------------------------------------------------------
procedure Layout;
---------------------------------------------------------------------
-- Position_Of
--
-- Purpose:
-- This function returns the coordinate position of the
-- specified node. The default position (0,0) is returned if no
-- position has been assigned yet.
-- Exceptions:
-- Node_Not_Defined
---------------------------------------------------------------------
function Position_Of (Current : in Node)
return Position;
...
end Autolayout;
rationale
The purpose of a header comment on the specification of a program unit is to help the user understand how to use the program unit. From reading the program unit specification and header, a user should know everything necessary to use the unit. It should not be necessary to read the body of the program unit. Therefore, there should be a header comment on each program unit specification, and each header should contain all usage information not expressed in the specification itself. Such information includes the units' effects on each other and on shared resources, exceptions raised, and time/space characteristics. None of this information can be determined from the Ada specification of the program unit.
When you duplicate information in the header that can be readily obtained from the specification, the information tends to become incorrect during maintenance. For example, do not make a point of listing all parameter names, modes, or subtypes when describing a procedure. This information is already available from the procedure specification. Similarly, do not list all subprograms of a package in the header unless this is necessary to make some important statement about the subprograms.
Do not include information in the header that the user of the program unit does not need. In particular, do not include information about how a program unit performs its function or why a particular algorithm was used. This information should be hidden in the body of the program unit to preserve the abstraction defined by the unit. If the user knows such details and makes decisions based on that information, the code may suffer when that information is later changed.
When describing the purpose of the unit, avoid referring to other parts of the enclosing software system. It is better to say "this unit does…" than to say "this unit is called by Xyz to do…." The unit should be written in such a way that it does not know or care which unit is calling it. This makes the unit much more general purpose and reusable. In addition, information about other units is likely to become obsolete and incorrect during maintenance.
Include information about the performance (time and space) characteristics of the unit. Much of this information is not present in the Ada specification, but it is required by the user. To integrate the unit into a system, the user needs to understand the resource usage (CPU, memory, etc.) of the unit. It is especially important to note that when a subprogram call causes activation of a task hidden in a package body, the task may continue to consume resources after the subroutine ends.
notes
Some projects have deferred most of the commentary to the end rather than at the beginning of the program unit. Their rationale is that program units are written once and read many times and that long header comments make the start of the specification difficult to find.
exceptions
Where a group of program units are closely related or simple to understand, it is acceptable to use a single header for the entire group of program units. For example, it makes sense to use a single header to describe the behavior of Max and Min functions; Sin, Cos, and Tan functions; or a group of functions to query related attributes of an object encapsulated in a package. This is especially true when each function in the set is capable of raising the same exceptions.
Program Unit Body Headers
guideline
- Place information required by the maintainer of the program unit in the body of the header
- Explain how and why the unit performs its function, not what the unit does.
- Do not repeat information (except unit name) in the header that is readily apparent from reading the code.
- Do not repeat information (except unit name) in the body header that is available in the specification header.
instantiation
- Put the name of the program unit in the header.
- Record portability issues in the header.
- Summarize complex algorithms in the header.
- Record reasons for significant or controversial implementation decisions.
- Record discarded implementation alternatives, along with the reason for discarding them.
- Record anticipated changes in the header, especially if some work has already been done to the code to make the changes easy to accomplish.
example
------------------------------------------------------------------------
-- Autolayout
--
-- Implementation Notes:
-- - This package uses a heuristic algorithm to minimize the number
-- of arc crossings. It does not always achieve the true minimum
-- number which could theoretically be reached. However it does a
-- nearly perfect job in relatively little time. For details about
-- the algorithm, see ...
--
-- Portability Issues:
-- - The native math package Math_Lib is used for computations of
-- coordinate positions.
-- - 32-bit integers are required.
-- - No operating system specific routines are called.
--
-- Anticipated Changes:
-- - Coordinate_Type below could be changed from integer to float
-- with little effort. Care has been taken to not depend on the
-- specific characteristics of integer arithmetic.
------------------------------------------------------------------------
package body Autolayout is
...
---------------------------------------------------------------------
-- Define
--
-- Implementation Notes:
-- - This routine stores a node in the general purpose Graph data
-- structure, not the Fast_Graph structure because ...
---------------------------------------------------------------------
procedure Define
(New_Node : in Node) is
begin
...
end Define;
---------------------------------------------------------------------
-- Layout
--
-- Implementation Notes:
-- - This routine copies the Graph data structure (optimized for
-- fast random access) into the Fast_Graph data structure
-- (optimized for fast sequential iteration), then performs the
-- layout, and copies the data back to the Graph structure. This
-- technique was introduced as an optimization when the algorithm
-- was found to be too slow, and it produced an order of
-- magnitude improvement.
---------------------------------------------------------------------
procedure Layout is
begin
...
end Layout;
---------------------------------------------------------------------
-- Position_Of
---------------------------------------------------------------------
function Position_Of (Current : in Node)
return Position is
begin
...
end Position_Of;
...
end Autolayout;
rationale
The purpose of a header comment on the body of a program unit is to help the maintainer of the program unit to understand the implementation of the unit, including tradeoffs among different techniques. Be sure to document all decisions made during implementation to prevent the maintainer from making the same mistakes you made. One of the most valuable comments to a maintainer is a clear description of why a change being considered will not work.
The header is also a good place to record portability concerns. The maintainer may have to port the software to a different environment and will benefit from a list of nonportable features. Furthermore, the act of collecting and recording portability issues focuses attention on these issues and may result in more portable code from the start.
Summarize complex algorithms in the header if the code is difficult to read or understand without such a summary, but do not merely paraphrase the code. Such duplication is unnecessary and hard to maintain. Similarly, do not repeat the information from the header of the program unit specification.
notes
It is often the case that a program unit is self-explanatory so that it does not require a body header to explain how it is implemented or why. In such a case, omit the header entirely, as in the case with Position_Of above. Be sure, however, that the header you omit truly contains no information. For example, consider the difference between the two header sections:
-- Implementation Notes: None.
and:
-- NonPortable Features: None.
The first is a message from the author to the maintainer saying "I can't think of anything else to tell you," while the second may mean "I guarantee that this unit is entirely portable."
Data Comments
guideline
- Comment on all data types, objects, and exceptions unless their names are self-explanatory.
- Include information on the semantic structure of complex, pointer-based data structures.
- Include information about relationships that are maintained between data objects.
- Omit comments that merely repeat the information in the name.
- Include information on redispatching for tagged types in cases where you intend the specializations (i.e., derived types) to override these redispatching operations.
example
Objects can be grouped by purpose and commented as:
...
---------------------------------------------------------------------
-- Current position of the cursor in the currently selected text
-- buffer, and the most recent position explicitly marked by the
-- user.
-- Note: It is necessary to maintain both current and desired
-- column positions because the cursor cannot always be
-- displayed in the desired position when moving between
-- lines of different lengths.
---------------------------------------------------------------------
Desired_Column : Column_Counter;
Current_Column : Column_Counter;
Current_Row : Row_Counter;
Marked_Column : Column_Counter;
Marked_Row : Row_Counter;
The conditions under which an exception is raised should be commented:
---------------------------------------------------------------------
-- Exceptions
---------------------------------------------------------------------
Node_Already_Defined : exception; -- Raised when an attempt is made
--| to define a node with an
--| identifier which already
--| defines a node.
Node_Not_Defined : exception; -- Raised when a reference is
--| made to a node which has
--| not been defined.
Here is a more complex example, involving multiple record and access types that are used to form a complex data structure:
---------------------------------------------------------------------
-- These data structures are used to store the graph during the
-- layout process. The overall organization is a sorted list of
-- "ranks," each containing a sorted list of nodes, each containing
-- a list of incoming arcs and a list of outgoing arcs.
-- The lists are doubly linked to support forward and backward
-- passes for sorting. Arc lists do not need to be doubly linked
-- because order of arcs is irrelevant.
--
-- The nodes and arcs are doubly linked to each other to support
-- efficient lookup of all arcs to/from a node, as well as efficient
-- lookup of the source/target node of an arc.
---------------------------------------------------------------------
type Arc;
type Arc_Pointer is access Arc;
type Node;
type Node_Pointer is access Node;
type Node is
record
Id : Node_Pointer;-- Unique node ID supplied by the user.
Arc_In : Arc_Pointer;
Arc_Out : Arc_Pointer;
Next : Node_Pointer;
Previous : Node_Pointer;
end record;
type Arc is
record
ID : Arc_ID; -- Unique arc ID supplied by the user.
Source : Node_Pointer;
Target : Node_Pointer;
Next : Arc_Pointer;
end record;
type Rank;
type Rank_Pointer is access Rank;
type Rank is
record
Number : Level_ID; -- Computed ordinal number of the rank.
First_Node : Node_Pointer;
Last_Node : Node_Pointer;
Next : Rank_Pointer;
Previous : Rank_Pointer;
end record;
First_Rank : Rank_Pointer;
Last_Rank : Rank_Pointer;
rationale
It is very useful to add comments explaining the purpose, structure, and semantics of the data structures. Many maintainers look at the data structures first when trying to understand the implementation of a unit. Understanding the data that can be stored, along with the relationships between the different data items and the flow of data through the unit, is an important first step in understanding the details of the unit.
In the first example above, the names Current_Column and Current_Row are relatively self-explanatory. The name Desired_Column is also well chosen, but it leaves the reader wondering what the relationship is between the current column and the desired column. The comment explains the reason for having both.
Another advantage of commenting on the data declarations is that the single set of comments on a declaration can replace multiple sets of comments that might otherwise be needed at various places in the code where the data is manipulated. In the first example above, the comment briefly expands on the meaning of "current" and "marked." It states that the "current" position is the location of the cursor, the "current" position is in the current buffer, and the "marked" position was marked by the user. This comment, along with the mnemonic names of the variables, greatly reduces the need for comments at individual statements throughout the code.
It is important to document the full meaning of exceptions and under what conditions they can be raised, as shown in the second example above, especially when the exceptions are declared in a package specification. The reader has no other way to find out the exact meaning of the exception (without reading the code in the package body).
Grouping all the exceptions together, as shown in the second example, can provide the reader with the effect of a "glossary" of special conditions. This is useful when many different subprograms in the package can raise the same exceptions. For a package in which each exception can be raised by only one subprogram, it may be better to group related subprograms and exceptions together.
When commenting exceptions, it is better to describe the exception's meaning in general terms than to list all the subprograms that can cause the exception to be raised; such a list is harder to maintain. When a new routine is added, it is likely that these lists will not be updated. Also, this information is already present in the comments describing the subprograms, where all exceptions that can be raised by the subprogram should be listed. Lists of exceptions by subprogram are more useful and easier to maintain than lists of subprograms by exception.
In the third example, the names of the record fields are short and mnemonic, but they are not completely self-explanatory. This is often the case with complex data structures involving access types. There is no way to choose the record and field names so that they completely explain the overall organization of the records and pointers into a nested set of sorted lists. The comments shown are useful in this case. Without them, the reader would not know which lists are sorted, which lists are doubly linked, or why. The comments express the intent of the author with respect to this complex data structure. The maintainer still has to read the code if he wants to be sure that the double links are all properly maintained. Keeping this in mind when reading the code makes it much easier for the maintainer to find a bug where one pointer is updated and the opposite one is not.
See Guideline 9.3.1 for the rationale for documenting the use of redispatching operations. (Redispatching means converting an argument of one primitive operation to a class-wide type and making a dispatching call to another primitive operation.) The rationale in Guideline 9.3.1 discusses whether such documentation should be in the specification or the body.
Statement Comments
guideline
- Minimize comments embedded among statements.
- Use comments only to explain parts of the code that are not obvious.
- Comment intentional omissions from the code.
- Do not use comments to paraphrase the code.
- Do not use comments to explain remote pieces of code, such as subprograms called by the current unit.
- Where comments are necessary, make them visually distinct from the code.
example
The following is an example of very poorly commented code:
...
-- Loop through all the strings in the array Strings, converting
-- them to integers by calling Convert_To_Integer on each one,
-- accumulating the sum of all the values in Sum, and counting them
-- in Count. Then divide Sum by Count to get the average and store
-- it in Average. Also, record the maximum number in the global
-- variable Max_Number.
for I in Strings'Range loop
-- Convert each string to an integer value by looping through
-- the characters which are digits, until a nondigit is found,
-- taking the ordinal value of each, subtracting the ordinal value
-- of '0', and multiplying by 10 if another digit follows. Store
-- the result in Number.
Number := Convert_To_Integer(Strings(I));
-- Accumulate the sum of the numbers in Total.
Sum := Sum + Number;
-- Count the numbers.
Count := Count + 1;
-- Decide whether this number is more than the current maximum.
if Number > Max_Number then
-- Update the global variable Max_Number.
Max_Number := Number;
end if;
end loop;
-- Compute the average.
Average := Sum / Count;
The following is improved by not repeating things in the comments that are obvious from the code, not describing the details of what goes in inside of Convert_To_Integer
, deleting an erroneous comment (the one on the statement that accumulates the sum), and making the few remaining comments more visually distinct from the code.
Sum_Integers_Converted_From_Strings:
for I in Strings'Range loop
Number := Convert_To_Integer(Strings(I));
Sum := Sum + Number;
Count := Count + 1;
-- The global Max_Number is computed here for efficiency.
if Number > Max_Number then
Max_Number := Number;
end if;
end loop Sum_Integers_Converted_From_Strings;
Average := Sum / Count;
rationale
The improvements shown in the example are not improvements merely by reducing the total number of comments; they are improvements by reducing the number of useless comments.
Comments that paraphrase or explain obvious aspects of the code have no value. They are a waste of effort for the author to write and the maintainer to update. Therefore, they often end up becoming incorrect. Such comments also clutter the code, hiding the few important comments.
Comments describing what goes on inside another unit violate the principle of information hiding. The details about Convert_To_Integer (deleted above) are irrelevant to the calling unit, and they are better left hidden in case the algorithm ever changes. Examples explaining what goes on elsewhere in the code are very difficult to maintain and almost always become incorrect at the first code modification.
The advantage of making comments visually distinct from the code is that it makes the code easier to scan, and the few important comments stand out better. Highlighting unusual or special code features indicates that they are intentional. This assists maintainers by focusing attention on code sections that are likely to cause problems during maintenance or when porting the program to another implementation.
Comments should be used to document code that is nonportable, implementation-dependent, environment-dependent, or tricky in any way. They notify the reader that something unusual was put there for a reason. A beneficial comment would be one explaining a work around for a compiler bug. If you use a lower level (not "ideal" in the software engineering sense) solution, comment on it. Information included in the comments should state why you used that particular construct. Also include documentation on the failed attempts, for example, using a higher level structure. This kind of comment is useful to maintainers for historical purposes. You show the reader that a significant amount of thought went into the choice of a construct.
Finally, comments should be used to explain what is not present in the code as well as what is present. If you make a conscious decision to not perform some action, like deallocating a data structure with which you appear to be finished, be sure to add a comment explaining why not. Otherwise, a maintainer may notice the apparent omission and "correct" it later, thus introducing an error.
See also Guideline 9.3.1 for a discussion of what kind of documentation you should provide regarding tagged types and redispatching.
notes
Further improvements can be made on the above example by declaring the variables Count and Sum in a local block so that their scope is limited and their initializations occur near their usage, e.g., by naming the block Compute_Average or by moving the code into a function called Average_Of. The computation of Max_Number can also be separated from the computation of Average. However, those changes are the subject of other guidelines; this example is only intended to illustrate the proper use of comments.
Marker Comments
guideline
- Use pagination markers to mark program unit boundaries (see Guideline 2.1.7).
- Repeat the unit name in a comment to mark the
begin
of a package body, subprogram body, task body, or block if thebegin
is preceded by declarations. - For long or heavily nested
if
andcase
statements, mark the end of the statement with a comment summarizing the condition governing the statement. - For long or heavily nested
if
statements, mark theelse
part with a comment summarizing the conditions governing this portion of the statement.
example
if A_Found then
...
elsif B_Found then
...
else -- A and B were both not found
...
if Count = Max then
...
end if;
...
end if; -- A_Found
------------------------------------------------------------------------
package body Abstract_Strings is
...
---------------------------------------------------------------------
procedure Concatenate (...) is
begin
...
end Concatenate;
---------------------------------------------------------------------
...
begin -- Abstract_Strings
...
end Abstract_Strings;
------------------------------------------------------------------------
rationale
Marker comments emphasize the structure of code and make it easier to scan. They can be lines that separate sections of code or descriptive tags for a construct. They help the reader resolve questions about the current position in the code. This is more important for large units than for small ones. A short marker comment fits on the same line as the reserved word with which it is associated. Thus, it adds information without clutter.
The if
, elsif
, else
, and end if
of an if
statement are often separated by long sequences of statements, sometimes involving other if
statements. As shown in the first example, marker comments emphasize the association of the keywords of the same statement over a great visual distance. Marker comments are not necessary with the block statement and loop statement because the syntax of these statements allows them to be named with the name repeated at the end. Using these names is better than using marker comments because the compiler verifies that the names at the beginning and end match.
The sequence of statements of a package body is often very far from the first line of the package. Many subprogram bodies, each containing many begin
lines, may occur first. As shown in the second example, the marker comment emphasizes the association of the begin with the package.
notes
Repeating names and noting conditional expressions clutters the code if overdone. It is visual distance, especially page breaks, that makes marker comments beneficial.
Using Types
Strong typing promotes reliability in software. The type definition of an object defines all legal values and operations and allows the compiler to check for and identify potential errors during compilation. In addition, the rules of type allow the compiler to generate code to check for violations of type constraints at execution time. Using these Ada compiler's features facilitates earlier and more complete error detection than that which is available with less strongly typed languages.
Declaring Types
guideline
- Limit the range of scalar types as much as possible.
- Seek information about possible values from the application.
- Do not reuse any of the subtype names in package Standard.
- Use subtype declarations to improve program readability (Booch 1987).
- Use derived types and subtypes in concert (see Guideline 5.3.1).
example
subtype Card_Image is String (1 .. 80);
Input_Line : Card_Image := (others => ' ');
-- restricted integer type:
type Day_Of_Leap_Year is range 1 .. 366;
subtype Day_Of_Non_Leap_Year is Day_Of_Leap_Year range 1 .. 365;
By the following declaration, the programmer means, "I haven't the foggiest idea how many," but the actual base range will show up buried in the code or as a system parameter:
Employee_Count : Integer;
rationale
Eliminating meaningless values from the legal range improves the compiler's ability to detect errors when an object is set to an invalid value. This also improves program readability. In addition, it forces you to carefully think about each use of objects declared to be of the subtype.
Different implementations provide different sets of values for most of the predefined types. A reader cannot determine the intended range from the predefined names. This situation is aggravated when the predefined names are overloaded.
The names of an object and its subtype can clarify their intended use and document low-level design decisions. The example above documents a design decision to restrict the software to devices whose physical parameters are derived from the characteristics of punch cards. This information is easy to find for any later changes, thus enhancing program maintainability.
You can rename a type by declaring a subtype without a constraint (Ada Reference Manual 1995, §8.5 [Annotated]). You cannot overload a subtype name; overloading only applies to callable entities. Enumeration literals are treated as parameterless functions and so are included in this rule.
Types can have highly constrained sets of values without eliminating useful values. Usage as described in Guideline 5.3.1 eliminates many flag variables and type conversions within executable statements. This renders the program more readable while allowing the compiler to enforce strong typing constraints.
notes
Subtype declarations do not define new types, only constraints for existing types.
Any deviation from this guideline detracts from the advantages of the strong typing facilities of the Ada language.
exceptions
There are cases where you do not have a particular dependence on any range of numeric values. Such situations occur, for example, with array indices (e.g., a list whose size is not fixed by any particular semantics). See Guideline 7.2.1 for a discussion of appropriate uses of predefined types.
Enumeration Types
guideline
- Use enumeration types instead of numeric codes.
- Only if absolutely necessary, use representation clauses to match requirements of external devices.
example
Use:
type Color is (Blue, Red, Green, Yellow);
rather than:
Blue : constant := 1;
Red : constant := 2;
Green : constant := 3;
Yellow : constant := 4;
and add the following if necessary:
for Color use (Blue => 1,
Red => 2,
Green => 3,
Yellow => 4);
rationale
Enumerations are more robust than numeric codes; they leave less potential for errors resulting from incorrect interpretation and from additions to and deletions from the set of values during maintenance. Numeric codes are holdovers from languages that have no user-defined types.
In addition, Ada provides a number of attributes ('Pos, 'Val, 'Succ, 'Pred, 'Image, and 'Value) for enumeration types that, when used, are more reliable than user-written operations on encodings.
A numeric code may at first seem appropriate to match external values. Instead, these situations call for a representation clause on the enumeration type. The representation clause documents the "encoding." If the program is properly structured to isolate and encapsulate hardware dependencies (see Guideline 7.1.5), the numeric code ends up in an interface package where it can be easily found and replaced if the requirements change.
In general, avoid using representation clauses for enumeration types. When there is no obvious ordering of the enumeration literals, an enumeration representation can create portability problems if the enumeration type must be reordered to accommodate a change in representation order on the new platform.
Summary
spelling
- Use underscores to separate words in a compound name.
- Represent numbers in a consistent fashion.
- Represent literals in a radix appropriate to the problem.
- Use underscores to separate digits the same way commas or periods (or spaces for nondecimal bases) would be used in normal text.
- When using scientific notation, make the E consistently either uppercase or lowercase.
- In an alternate base, represent the alphabetic characters in either all uppercase or all lowercase.
- Make reserved words and other elements of the program visually distinct from each other.
- Do not use an abbreviation of a long word as an identifier where a shorter synonym exists.
- Use a consistent abbreviation strategy.
- Do not use ambiguous abbreviations.
- To justify its use, an abbreviation must save many characters over the full word.
- Use abbreviations that are well-accepted in the application domain.
- Maintain a list of accepted abbreviations, and use only abbreviations on that list.
naming conventions
- Choose names that are as self-documenting as possible.
- Use a short synonym instead of an abbreviation.
- Use names given by the application, but do not use obscure jargon.
- Avoid using the same name to declare different kinds of identifiers.
- Use singular, general nouns as subtype identifiers.
- Choose identifiers that describe one of the subtype's values.
- Consider using suffixes for subtype identifiers that define visible access types, visible subranges, or visible array types.
- For private types, do not use identifier constructions (e.g., suffixes) that are unique to subtype identifiers.
- Do not use the subtype names from predefined packages.
- Use predicate clauses or adjectives for Boolean objects.
- Use singular, specific nouns as object identifiers.
- Choose identifiers that describe the object's value during execution.
- Use singular, general nouns as identifiers for record components.
- Use a consistent naming convention for tagged types and associated packages.
- Use action verbs for procedures and entries.
- Use predicate clauses for Boolean functions.
- Use nouns for non-Boolean functions.
- Give packages names that imply a higher level of organization than subprograms. Generally, these are noun phrases that describe the abstraction provided.
- Give tasks names that imply an active entity.
- Use nouns descriptive of the data being protected for protected units.
- Consider naming generic subprograms as if they were nongeneric subprograms.
- Consider naming generic packages as if they were nongeneric packages.
- Make the generic names more general than the instantiated names.
- Use symbolic values instead of literals, wherever possible.
- Use the predefined constants Ada.Numerics.Pi and Ada.Numerics.e for the mathematical constants Pi and e.
- Use constants instead of variables for constant values.
- Use a constant when the value is specific to a type or when the value must be static.
- Use named numbers instead of constants, whenever possible.
- Use named numbers to replace numeric literals whose type or context is truly universal.
- Use constants for objects whose values cannot change after elaboration. (United Technologies 1987).
- Show relationships between symbolic values by defining them with static expressions.
- Use linearly independent sets of literals.
- Use attributes like 'First and 'Last instead of literals, wherever possible.
- Use a name that indicates the kind of problem the exception represents.
- Include a prefix like New, Make, or Create in naming constructors (in this sense, operations to create and/or initialize an object).
- Use names indicative of their content for child packages containing constructors.
comments
- Make the code as clear as possible to reduce the need for comments.
- Never repeat information in a comment that is readily available in the code.
- Where a comment is required, make it concise and complete.
- Use proper grammar and spelling in comments.
- Make comments visually distinct from the code.
- Structure comments in headers so that information can be automatically extracted by a tool.
- Put a file header on each source file.
- Place ownership, responsibility, and history information for the file in the file header.
- Put a header on the specification of each program unit.
- Place information required by the user of the program unit in the specification header.
- Do not repeat information (except unit name) in the specification header that is present in the specification.
- Explain what the unit does, not how or why it does it.
- Describe the complete interface to the program unit, including any exceptions it can raise and any global effects it can have.
- Do not include information about how the unit fits into the enclosing software system.
- Describe the performance (time and space) characteristics of the unit.
- Place information required by the maintainer of the program unit in the body header.
- Explain how and why the unit performs its function, not what the unit does.
- Do not repeat information (except unit name) in the header that is readily apparent from reading the code.
- Do not repeat information (except unit name) in the body header that is available in the specification header.
- Comment on all data types, objects, and exceptions unless their names are self-explanatory.
- Include information on the semantic structure of complex, pointer-based data structures.
- Include information about relationships that are maintained between data objects.
- Omit comments that merely repeat the information in the name.
- Include information on redispatching for tagged types in cases where you intend the specializations (i.e., derived types) to override these redispatching operations.
- Minimize comments embedded among statements.
- Use comments only to explain parts of the code that are not obvious.
- Comment intentional omissions from the code.
- Do not use comments to paraphrase the code.
- Do not use comments to explain remote pieces of code, such as subprograms called by the current unit.
- Where comments are necessary, make them visually distinct from the code.
- Use pagination markers to mark program unit boundaries.
- Repeat the unit name in a comment to mark the begin of a package body, subprogram body, task body, or block if the begin is preceded by declarations.
- For long or heavily nested if and case statements, mark the end of the statement with a comment summarizing the condition governing the statement.
- For long or heavily nested if statements, mark the else part with a comment summarizing the conditions governing this portion of the statement.
using types
- Limit the range of scalar types as much as possible.
- Seek information about possible values from the application.
- Do not reuse any of the subtype names in package Standard.
- Use subtype declarations to improve program readability (Booch 1987).
- Use derived types and subtypes in concert.
- Use enumeration types instead of numeric codes.
- Only if absolutely necessary, use representation clauses to match requirements of external devices.
Program Structure
Introduction
Proper structure improves program clarity. This is analogous to readability on lower levels and facilitates the use of the readability guidelines (Chapter 3). The various program structuring facilities provided by Ada were designed to enhance overall clarity of design. These guidelines show how to use these facilities for their intended purposes.
The concept of child packages supports the concept of subsystem, where a subsystem is represented in Ada as a hierarchy of library units. In general, a large system should be structured as a series of subsystems. Subsystems should be used to represent logically related library units, which together implement a single, high-level abstraction or framework.
Abstraction and encapsulation are supported by the package concept and by private types. Related data and subprograms can be grouped together and seen by a higher level as a single entity. Information hiding is enforced via strong typing and by the separation of package and subprogram specifications from their bodies. Exceptions and tasks are additional Ada language elements that impact program structure.
High-Level Structure
Well-structured programs are easily understood, enhanced, and maintained. Poorly structured programs are frequently restructured during maintenance just to make the job easier. Many of the guidelines listed below are often given as general program design guidelines.
Separate Compilation Capabilities
guideline
- Place the specification of each library unit package in a separate file from its body.
- Avoid defining library unit subprograms that are not intended to be used as main programs. If such subprograms are defined, then create an explicit specification, in a separate file, for each library unit subprogram.
- Minimize the use of subunits.
- In preference to subunits, use child library units to structure a subsystem into manageable units.
- Place each subunit in a separate file.
- Use a consistent file naming convention.
- In preference to nesting in a package body, use a private child and with it to the parent body.
- Use private child unit specifications for data and subprograms that are required by (other) child units that extend a parent unit's abstraction or services.
example
The file names below illustrate one possible file organization and associated consistent naming convention. The library unit name uses the adb suffix for the body. The suffix ads indicates the specification, and any files containing subunits use names constructed by separating the body name from the subunit name with an underscore:
text_io.ads — the specification text_io.adb — the body text_io_integer_io.adb — a subunit text_io_fixed_io.adb — a subunit text_io_float_io.adb — a subunit text_io_enumeration_io.adb — a subunit
Depending on what characters your file system allows you to use in file names, you could show the distinction between parent and subunit name more clearly in the file name. If your file system allows the "#" character, for example, you could separate the body name from the subunit name with a #:
text_io.ads — the specification text_io.adb — the body text_io#integer_io.adb — a subunit text_io#fixed_io.adb — a subunit text_io#float_io.adb — a subunit text_io#enumeration_io.adb — a subunit
Some operating systems are case-sensitive, although Ada itself is not a case-sensitive language. For example, you could choose a convention of all lowercase file names.
rationale
The main reason for the emphasis on separate files in this guideline is to minimize the amount of recompilation required after each change. Typically, during software development, bodies of units are updated far more often than specifications. If the body and specification reside in the same file, then the specification will be compiled each time the body is compiled, even though the specification has not changed. Because the specification defines the interface between the unit and all of its users, this recompilation of the specification typically makes recompilation of all users necessary in order to verify compliance with the specification. If the specifications and bodies of the users also reside together, then any users of these units will also have to be recompiled and so on. The ripple effect can force a huge number of compilations that could have been avoided, severely slowing the development and test phase of a project. This is why you should place specifications of all library units (nonnested units) in separate files from their bodies.
Library unit subprograms should be minimized. The only real use for library unit subprograms is as the main subprogram. In almost all other cases, it is better to embed the subprogram into a package. This provides a place (the package body) to localize data needed by the subprogram. Moreover, it cuts down on the number of separate modules in the system.
In general, you should use a separate specification for any library subprogram that is mentioned in a with clause. This makes the with'ing unit dependent on the library subprogram specification, not its body.
You should minimize the use of subunits because they create maintenance problems. Declarations appearing in the parent body are visible in the subunit, increasing the amount of data global to the subunit and, thus, increasing the potential ripple effect of changes. Subunits hinder reuse because they provide an incentive to put otherwise reusable code in the subunit directly rather than in a common routine called from multiple subprograms.
With the availability of child library units in Ada 95, you can avoid most uses of subunits. For example, instead of using a subunit for a large nested body, you should try to encapsulate this code in a child library unit and add the necessary context clauses. You can modify the body of the child unit without having to recompile any of the other units in a subsystem.
An additional benefit of using multiple, separate files is that it allows different implementors to modify different parts of the system at the same time with conventional editors, which do not allow multiple concurrent updates to a single file.
Finally, keeping bodies and specifications separate makes it possible to have multiple bodies for the same specification or multiple specifications for the same body. Although Ada requires that there be exactly one specification per body in a system at any given time, it can still be useful to maintain multiple bodies or multiple specifications for use in different builds of a system. For example, a single specification may have multiple bodies, each of which implements the same functionality with a different tradeoff of time versus space efficiency, or, for machine-dependent code, there may be one body for each target machine. Maintaining multiple package specifications can also be useful during development and test. You may develop one specification for delivery to your customer and another for unit testing. The first one would export only those subprograms intended to be called from outside of the package during normal operation of the system. The second one would export all subprograms of the package so that each of them could be independently tested.
A consistent file naming convention is recommended to make it easier to manage the large number of files that may result from following this guideline.
In implementing the abstraction defined in a package specification, you often need to write supporting subprograms that manipulate the internal representation of the data. These subprograms should not be exported on the interface. You have a choice of whether to place them in the package body of the parent program or in a child package named in a context clause of the parent package body. When you place them in the parent package body, you make them inaccessible to all clients of the parent, including extensions of the parent declared in child packages. If these subprograms are needed to implement extensions of the parent abstraction, you would be forced to modify both the parent specification and the body because you would have to declare the extensions within the parent specification. This technique would then force recompilation of the entire package (specification and body) as well as all its clients.
Alternatively, you can implement the supporting subprograms in a private child package. Because the parent unit's specification is not modified, neither it nor its clients need to be recompiled. The data and subprograms that might have declared in the parent unit body must now be declared in the private child unit's specification to make them visible to both the parent unit body and to any child units that extend the parent unit's services or abstractions. (See also Guidelines 4.1.6 and 4.2.) This use of private child units will generally minimize recompilations within the unit family and among its clients.
In declaring the child package private, you achieve a similar effect to declaring it in the parent package body to the extent that clients of the parent cannot name the private child in a context clause. You gain flexibility because now you can extend the parent abstraction using child packages without having to recompile the parent specification or its body, assuming that you do not otherwise modify the parent or its body. This added flexibility will usually compensate for the increased dependency between units, in this case, the additional context clause on the parent body (and other child package bodies) that names the private child package of supporting subprograms.
Configuration Pragmas
guideline
- When possible, express configuration pragmas through compiler options or other means that do not require modifications to the source code.
- When configuration pragmas must be placed in source code, consider isolating them to one compilation unit per partition; if specified, the main subprogram for the partition is recommended.
rationale
Configuration pragmas are generally used to select a partition-wide or system-wide option. Usually, they reflect either high-level software architecture decisions (e.g., pragma Task_Dispatching_Policy) or the use of the software in a particular application domain (e.g., safety-critical software). If a configuration pragma is embedded within a software component and that component is reused in a different context where the pragma is no longer appropriate, then it may cause problems in the new application. Such problems can include the rejection by the compilation system of otherwise legal source code or unexpected behavior at run-time. These problems can be significant given the wide scope of a configuration pragma. In addition, maintenance of the original system may require that some of these system-wide decisions be changed. If the configuration pragmas are scattered throughout the software, it may be difficult to locate the lines that need to change.
As a result, it is recommended that all configuration pragmas be kept in a single compilation unit if possible to make them easy to locate and modify as needed. If this compilation unit is unlikely to be reused (e.g., a main subprogram), then the likelihood of conflicts with future reusers is reduced. Finally, if these system-wide decisions are indicated without embedding them in the code at all, such as through a compiler option, then the problems described above are even less likely to occur.
exceptions
Certain pragmas (e.g., pragma Suppress) can be used in several forms, including as a configuration pragma. This guideline does not apply to such pragmas when they are not used as a configuration pragma.
Subprograms
guideline
- Use subprograms to enhance abstraction.
- Restrict each subprogram to the performance of a single action.
example
Your program is required to draw a menu of user options as part of a menu-driven user interface package. Because the contents of the menu can vary depending on the user state, the proper way to do this is to write a subprogram to draw the menu. This way, the output subprogram has one purpose and the way to determine the menu content is described elsewhere.
...
----------------------------------------------------------------------
procedure Draw_Menu
(Title : in String;
Options : in Menu) is
...
begin -- Draw_Menu
Ada.Text_IO.New_Page;
Ada.Text_IO.New_Line;
Ada.Text_IO.Set_Col (Right_Column);
Ada.Text_IO.Put_Line (Title);
Ada.Text_IO.New_Line;
for Choice in Alpha_Numeric loop
if Options (Choice) /= Empty_Line then
Valid_Option (Choice) := True;
Ada.Text_IO.Set_Col (Left_Column);
Ada.Text_IO.Put (Choice & " -- ");
Ada.Text_IO.Put_Line (Options (Choice));
end if;
...
end loop;
end Draw_Menu;
----------------------------------------------------------------------
rationale
Subprograms are an extremely effective and well-understood abstraction technique. Subprograms increase program readability by hiding the details of a particular activity. It is not necessary that a subprogram be called more than once to justify its existence.
notes
Guideline 10.7.1 discusses dealing with the overhead of subroutine calls.
Functions
guideline
- Use a function when the subprogram's primary purpose is to provide a single value.
- Minimize the side effect of a function.
- Consider using a parameterless function when the value does not need to be static.
- Use a parameterless function (instead of a constant) if the value should be inherited by types derived from the type.
- Use a parameterless function if the value itself is subject to change.
example
Although reading a character from a file will change what character is read next, this is accepted as a minor side effect compared to the primary purpose of the following function:
function Next_Character return Character is separate;
However, the use of a function like this could lead to a subtle problem. Any time the order of evaluation is undefined, the order of the values returned by the function will effectively be undefined. In this example, the order of the characters placed in Word and the order that the following two characters are given to the Suffix parameters are unknown. No implementation of the Next_Character function can guarantee which character will go where:
Word : constant String := String'(1 .. 5 => Next_Character);
begin -- Start_Parsing
Parse(Keyword => Word,
Suffix1 => Next_Character,
Suffix2 => Next_Character);
end Start_Parsing;
Of course, if the order is unimportant (as in a random number generator), then the order of evaluation is unimportant.
The following example shows the use of a parameterless function instead of a constant:
type T is private;
function Nil return T; -- This function is a derivable operation of type T
function Default return T; -- Also derivable, and the value can be changed by
-- recompiling the body of the function
This same example could have been written using constants:
type T is private;
Nil : constant T;
Default : constant T;
rationale
A side effect is a change to any variable that is not local to the subprogram. This includes changes to variables by other subprograms and entries during calls from the function if the changes persist after the function returns. Side effects are discouraged because they are difficult to understand and maintain. Additionally, the Ada language does not define the order in which functions are evaluated when they occur in expressions or as actual parameters to subprograms. Therefore, a program that depends on the order in which side effects of functions occur is erroneous. Avoid using side effects anywhere.
Packages
guideline
- Use packages for information hiding.
- Use packages with tagged types and private types for abstract data types.
- Use packages to model abstract entities appropriate to the problem domain.
- Use packages to group together related type and object declarations (e.g., common declarations for two or more library units).
- Encapsulate machine dependencies in packages. Place a software interface to a particular device in a package to facilitate a change to a different device.
- Place low-level implementation decisions or interfaces in subprograms within packages.
- Use packages and subprograms to encapsulate and hide program details that may change (Nissen and Wallis 1984).
example
Reading the names and other attributes of external files is highly machine dependent. A package called Directory could contain type and subprogram declarations to support a generalized view of an external directory that contains external files. Its internals may, in turn, depend on other packages more specific to the hardware or operating system:
package Directory is
type Directory_Listing is limited private;
procedure Read_Current_Directory (D : in out Directory_Listing);
generic
with procedure Process (Filename : in String);
procedure Iterate (Over : in Directory_Listing);
...
private
type Directory_Listing is ...
end Directory;
---------------------------------------------------------------
package body Directory is
-- This procedure is machine dependent
procedure Read_Current_Directory (D : in out Directory_Listing) is separate;
procedure Iterate (Over : in Directory_Listing) is
...
begin
...
Process (Filename);
...
end Iterate;
...
end Directory;
rationale
Packages are the principal structuring facility in Ada. They are intended to be used as direct support for abstraction, information hiding, and modularization. For example, they are useful for encapsulating machine dependencies as an aid to portability. A single specification can have multiple bodies isolating implementation-specific information so other parts of the code do not need to change.
Encapsulating areas of potential change helps to minimize the effort required to implement that change by preventing unnecessary dependencies among unrelated parts of the system.
notes
The most prevalent objection to this guideline usually involves performance penalties. See Guideline 10.7.1 for a discussion about subprogram overhead.
Child Library Units
guideline
- If a new library unit represents a logical extension to the original abstraction, define it as a child library unit.
- If a new library unit is independent (e.g., introduces a new abstraction that depends only in part on the existing one), then encapsulate the new abstraction in a separate library unit.
- Use child packages to implement a subsystem.
- Use public child units for those parts of a subsystem that should be visible to clients of the subsystem.
- Use private child units for those parts of a subsystem that should not be visible to clients of the subsystem.
- Use private child units for local declarations used only in implementing the package specification.
- Use child packages to implement constructors, even when they return access values.
example
The following example of a windowing system is taken from Cohen et al. (1993) and illustrates some of the uses of child units in designing subsystems. The parent (root) package declares the types, subtypes, and constants that its clients and subsystems need. Individual child packages provide specific parts of the windowing abstraction, such as atoms, fonts, graphic output, cursors, and keyboard information:
package X_Windows is
...
private
...
end X_Windows;
package X_Windows.Atoms is
type Atom is private;
...
private
...
end X_Windows.Atoms;
package X_Windows.Fonts is
type Font is private;
...
private
...
end X_Windows.Fonts;
package X_Windows.Graphic_Output is
type Graphic_Context is private;
type Image is private;
...
private
...
end X_Windows.Graphic_Output;
package X_Windows.Cursors is
...
end X_Windows.Cursors;
package X_Windows.Keyboard is
...
end X_Windows.Keyboard;
rationale
The user can create more precise packages with less cluttered interfaces, using child library packages to extend the interfaces as needed. The parent contains only the relevant functionality. The parent provides a general-purpose interface, while the child units provide more complete programming interfaces, tailored to that aspect of an abstraction that they are extending or defining.
Child packages build on the modular strength of Ada where "the distinct specification and body decouple the user interface to a package (the specification) from its implementation (the body)" (Rationale 1995, §II.7). Child packages provide the added capability of being able to extend a parent package without recompiling the parent or the parent's clients.
Child packages allow you to write logically distinct packages that share a private type. The visibility rules give the private part of the child specification and the body of the child visibility into the private part of the parent. Thus, you can avoid creating a monolithic package for the sake of developing abstractions that share a private type and need to know its representation. The private representation is not available to clients of the package, so the abstraction in the package and its children is maintained.
Using private child packages for local declarations enables you to have available the support declarations you need when implementing both the parent package and extensions to the parent package. You enhance the maintainability of your program by using a common set of support declarations (data representations, data manipulation subprograms). You can modify the internal representation and the implementation of the support subprograms without modifying or recompiling the rest of your subsystem because these support subprograms are implemented in the body of the private child package. See also Guidelines 4.1.1, 4.2.1, 8.4.1, and 8.4.8.
See also Guideline 9.4.1 for a discussion of the use of child library units in creating a tagged type hierarchy.
Cohesion
guideline
- Make each package serve a single purpose.
- Use packages to group related data, types, and subprograms.
- Avoid collections of unrelated objects and subprograms (NASA 1987; Nissen and Wallis 1984).
- Consider restructuring a system to move two highly related units into the same package (or package hierarchy) or to move relatively independent units into separate packages.
example
As a bad example, a package named Project_Definitions is obviously a "catch all" for a particular project and is likely to be a jumbled mess. It probably has this form to permit project members to incorporate a single with clause into their software.
Better examples are packages called Display_Format_Definitions, containing all the types and constants needed by some specific display in a specific format, and Cartridge_Tape_Handler, containing all the types, constants, and subprograms that provide an interface to a special-purpose device.
rationale
The degree to which the entities in a package are related has a direct impact on the ease of understanding packages and programs made up of packages. There are different criteria for grouping, and some criteria are less effective than others. Grouping the class of data or activity (e.g., initialization modules) or grouping data or activities based on their timing characteristics is less effective than grouping based on function or need to communicate through data (Charette 1986).
The "correct" structuring of a system can make a tremendous difference in the maintainability of a system. Although it may seem painful at the time, it is important to restructure if the initial structuring is not quite right.
See also Guideline 5.4.2 on heterogeneous data.
notes
Traditional subroutine libraries often group functionally unrelated subroutines. Even such libraries should be broken into a collection of packages, each containing a logically cohesive set of subprograms.
Data Coupling
guideline
- Avoid declaring variables in package specifications.
example
This is part of a compiler. Both the package handling error messages and the package containing the code generator need to know the current line number. Rather than storing this in a shared variable of type Natural, the information is stored in a package that hides the details of how such information is represented and makes it available with access routines:
-------------------------------------------------------------------------
package Compilation_Status is
type Line_Number is range 1 .. 2_500_000;
function Source_Line_Number return Line_Number;
end Compilation_Status;
-------------------------------------------------------------------------
with Compilation_Status;
package Error_Message_Processing is
-- Handle compile-time diagnostic.
end Error_Message_Processing;
-------------------------------------------------------------------------
with Compilation_Status;
package Code_Generation is
-- Operations for code generation.
end Code_Generation;
-------------------------------------------------------------------------
rationale
Strongly coupled program units can be difficult to debug and very difficult to maintain. By protecting shared data with access functions, the coupling is lessened. This prevents dependence on the data structure, and access to the data can be controlled.
notes
The most prevalent objection to this guideline usually involves performance penalties. When a variable is moved to the package body, subprograms to access the variable must be provided and the overhead involved during each call to those subprograms is introduced. See Guideline 10.7.1 for a discussion about subprogram overhead.
Tasks
guideline
- Use tasks to model abstract, asynchronous entities within the problem domain.
- Use tasks to define concurrent algorithms for multiprocessor architectures.
- Use tasks to perform concurrent, cyclic, or prioritized activities (NASA 1987).
rationale
The rationale for this guideline is given under Guideline 6.1.2. Chapter 6 discusses tasking in more detail.
Protected Types
guideline
- Use protected types to control or synchronize access to data or devices.
- Use protected types to implement synchronization tasks, such as a passive resource monitor.
example
See example in Guideline 6.1.1.
rationale
The rationale for this guideline is given under Guideline 6.1.1. Chapter 6 discusses concurrency and protected types in more detail.
Visibility
Ada's ability to enforce information hiding and separation of concerns through its visibility controlling features is one of the most important advantages of the language, particularly when "pieces of a large system are being developed separately." Subverting these features, for example, by excessive reliance on the use clause, is wasteful and dangerous. See also Guidelines 5.7 and 9.4.1.
Minimization of Interfaces
guideline
- Put only what is needed for the use of a package into its specification.
- Minimize the number of declarations in package specifications.
- Do not include extra operations simply because they are easy to build.
- Minimize the context (with) clauses in a package specification.
- Reconsider subprograms that seem to require large numbers of parameters.
- Do not manipulate global data within a subprogram or package merely to limit the number of parameters.
- Avoid unnecessary visibility; hide the implementation details of a program unit from its users.
- Use child library units to control the visibility of parts of a subsystem interface.
- Use private child packages for those declarations that should not be used outside the subsystem.
- Use child library units present different views of an entity to different clients.
- Design (and redesign) interfaces after having worked out the logic of various expected clients of the interface.
example
-------------------------------------------------------------------------
package Telephone_Book is
type Listing is limited private;
procedure Set_Name (New_Name : in String;
Current : in out Listing);
procedure Insert (Name : in String;
Current : in out Listing);
procedure Delete (Obsolete : in String;
Current : in out Listing);
private
type Information;
type Listing is access Information;
end Telephone_Book;
-------------------------------------------------------------------------
package body Telephone_Book is
-- Full details of record for a listing
type Information is
record
...
Next : Listing;
end record;
First : Listing;
procedure Set_Name (New_Name : in String;
Current : in out Listing) is separate;
procedure Insert (Name : in String;
Current : in out Listing) is separate;
procedure Delete (Obsolete : in String;
Current : in out Listing) is separate;
end Telephone_Book;
-------------------------------------------------------------------------
rationale
For each entity in the specification, give careful consideration to whether it could be moved to a child package or to the parent package body. The fewer the extraneous details, the more understandable the program, package, or subprogram. It is important to maintainers to know exactly what a package interface is so that they can understand the effects of changes. Interfaces to a subprogram extend beyond the parameters. Any modification of global data from within a package or subprogram is an undocumented interface to the "outside" as well.
Minimize the context clauses on a specification by moving unnecessary clauses to the body. This technique makes the reader's job easier, localizes the recompilation required when library units change, and helps prevent a ripple effect during modifications. See also Guideline 4.2.3.
Subprograms with large numbers of parameters often indicate poor design decisions (e.g., the functional boundaries of the subprogram are inappropriate or parameters are structured poorly). Conversely, subprograms with no parameters are likely to be accessing global data.
Objects visible within package specifications can be modified by any unit that has visibility to them. The object cannot be protected or represented abstractly by its enclosing package. Objects that must persist should be declared in package bodies. Objects whose value depends on program units external to their enclosing package are probably either in the wrong package or are better accessed by a subprogram specified in the package specification.
Child library units can provide distinct views of the hierarchical library. The engineer can provide a different view for the client than for the implementor (Rationale 1995, §10.1). By creating private child packages, the engineer can provide facilities that are only available inside the subsystem rooted at the parent library unit. The declarations inside a private child package specification are not exported outside the subsystem. Thus, the engineer can declare utilities needed to implement an abstraction in a private child package (e.g., debugging utilities [Cohen et al. 1993]) and be certain that users of the abstraction (i.e., the clients) cannot access these utilities.
Different clients may have different needs for essentially the same resource. Instead of having multiple versions of the resources, consider having child units that export different views for different purposes.
Designing an interface based strictly on predicting what clients "might" need can produce a bloated and inappropriate interface. What then happens is that clients try to "live" with the interface and work around the inappropriate interfaces, repeating code that logically should be part of the shared abstraction. See Guideline 8.3.1 for a discussion of interfaces from the reusability perspective.
notes
In some cases, subroutine libraries look like large, monolithic packages. In such cases, it may be beneficial to break these up into smaller packages, grouping them according to category (e.g., trigonometric functions).
Nested Packages
guideline
- Use child packages rather than nested packages to present different views of the same abstraction.
- Nest package specifications within another package specification only for grouping operations or hiding common implementation details.
example
Annex A of the Ada Reference Manual (1995) gives an example of package specification nesting. The specification of the generic package Generic_Bounded_Length is nested inside the specification of package Ada.Strings.Bounded. The nested package is a generic, grouping closely related operations.
rationale
Grouping package specifications into an encompassing package emphasizes a relationship of commonality among those packages. It also allows them to share common implementation details resulting from the relationship. Nesting packages allows you to organize the name space of the package in contrast to the semantic effect of nesting inside of subprograms or task bodies.
An abstraction occasionally needs to present different views to different classes of users. Building one view upon another as an additional abstraction does not always suffice because the functionality of the operations presented by the views may be only partially disjointed. Nesting specifications groups the facilities of the various views, yet associates them with the abstraction they present. Abusive mixing of the views by another unit would be easy to detect due to the multiple use clauses or an incongruous mix of qualified names.
See the rationale discussed in Guideline 4.2.1.
Restricting Visibility
guideline
- Consider using private child packages in lieu of nesting.
- Restrict the visibility of program units as much as possible by nesting them inside package bodies (Nissen and Wallis 1984) if you cannot use a private child package.
- Minimize nesting program units inside subprograms and tasks.
- Minimize the scope within which with clauses apply.
- Only with those units directly needed.
example
This program illustrates the use of child library units to restrict visibility. The procedure Rational_Numbers.Reduce is nested inside the body of Rational_Numbers to restrict its visibility to the implementation of this abstraction. Rather than make the text input/output facilities visible to the entire rational number hierarchy, it is only available to the body of the child library Rational_Numbers.IO. This example is adapted from the Ada Reference Manual (1995, §§7.1 [Annotated], 7.2 [Annotated], and 10.1.1 [Annotated]):
-------------------------------------------------------------------------
package Rational_Numbers is
type Rational is private;
function "=" (X, Y: Rational) return Boolean;
function "/" (X, Y: Integer) return Rational; -- construct a rational number
function "+" (X, Y: Rational) return Rational;
function "-" (X, Y: Rational) return Rational;
function "*" (X, Y: Rational) return Rational;
function "/" (X, Y: Rational) return Rational; -- rational division
private
...
end Rational_Numbers;
package body Rational_Numbers is
procedure Reduce (R :in out Rational) is . . . end Reduce;
. . .
end Rational_Numbers;
package Rational_Numbers.IO is
procedure Put (R : in Rational);
procedure Get (R : out Rational);
end Rational_Numbers.IO;
with Ada.Text_IO;
with Ada.Integer_Text_IO;
package body Rational_Numbers.IO is -- has visibility to parent private type declaration
procedure Put (R : in Rational) is
begin
Ada.Integer_Text_IO.Put (Item => R.Numerator, Width => 0);
Ada.Text_IO.Put ("/");
Ada.Integer_Text_IO.Put (Item => R.Denominator, Width => 0);
end Put;
procedure Get (R : out Rational) is . . . end Get;
end Rational_Numbers.IO;
rationale
Restricting visibility of a program unit ensures that the program unit is not called from some part of the system other than that which was intended. This is done by nesting it inside the only unit that uses it, by hiding it inside a package body rather than declaring it in the package specification, or by declaring it as a private child unit. This avoids errors and eases the job of maintainers by guaranteeing that a local change in that unit will not have an unforeseen global effect.
Restricting visibility of a library unit by using with clauses on subunits rather than on the entire parent unit is useful in the same way. In the example above, it is clear that the package Text_IO is used only by the Listing_Facilities package of the compiler.
Nesting inside subprograms and tasks is discouraged because it leads to unreusable components. These components are essentially unreusable because they make undesirable up-level references into the defining context. Unless you truly want to ensure that the program unit is not called from some unintended part of the system, you should minimize this form of nesting.
See also Guideline 4.2.1 for a discussion of the use of child units.
notes
One way to minimize the coverage of a with clause is to use it only with subunits that really need it. Consider making those subunits separate compilation units when the need for visibility to a library unit is restricted to a subprogram or two.
Hiding Tasks
guideline
- Carefully consider encapsulation of tasks.
example
-------------------------------------------------------------------------
package Disk_Head_Scheduler is
type Words is ...
type Track_Number is ...
procedure Transmit (Track : in Track_Number;
Data : in Words);
...
end Disk_Head_Scheduler;
-------------------------------------------------------------------------
package body Disk_Head_Scheduler is
...
task Control is
entry Sign_In (Track : in Track_Number);
...
end Control;
----------------------------------------------------------------------
task Track_Manager is
entry Transfer(Track_Number) (Data : in Words);
end Track_Manager;
----------------------------------------------------------------------
...
procedure Transmit (Track : in Track_Number;
Data : in Words) is
begin
Control.Sign_In(Track);
Track_Manager.Transfer(Track)(Data);
end Transmit;
----------------------------------------------------------------------
...
end Disk_Head_Scheduler;
-------------------------------------------------------------------------
rationale
The decision whether to declare a task in the specification or body of an enclosing package is not a simple one. There are good arguments for both.
Hiding a task specification in a package body and exporting (via subprograms ) only required entries reduces the amount of extraneous information in the package specification. It allows your subprograms to enforce any order of entry calls necessary to the proper operation of the tasks. It also allows you to impose defensive task communication practices (see Guideline 6.2.2) and proper use of conditional and timed entry calls. Finally, it allows the grouping of entries into sets for export to different classes of users (e.g., producers versus consumers) or the concealment of entries that should not be made public at all (e.g., initialization, completion, signals). Where performance is an issue and there are no ordering rules to enforce, the entries can be renamed as subprograms to avoid the overhead of an extra procedure call.
An argument, which can be viewed as an advantage or disadvantage, is that hiding the task specification in a package body hides the fact of a tasking implementation from the user. If the application is such that a change to or from a tasking implementation or a reorganization of services among tasks need not concern users of the package, then this is an advantage. However, if the package user must know about the tasking implementation to reason about global tasking behavior, then it is better not to hide the task completely. Either move it to the package specification or add comments stating that there is a tasking implementation, describing when a call may block, etc. Otherwise, it is the package implementor's responsibility to ensure that users of the package do not have to concern themselves with behaviors such as deadlock, starvation, and race conditions.
Finally, keep in mind that hiding tasks behind a procedural interface prevents the usage of conditional and timed entry calls and entry families, unless you add parameters and extra code to the procedures to make it possible for callers to direct the procedures to use these capabilities.
Exceptions
This section addresses the issue of exceptions in the context of program structures. It discusses how exceptions should be used as part of the interface to a unit, including what exceptions to declare and raise and under what conditions to raise them. Information on how to handle, propagate, and avoid raising exceptions is found in Guideline 5.8. Guidelines on how to deal with portability issues are in Guideline 7.5.
Using Exceptions to Help Define an Abstraction
guideline
- For unavoidable internal errors for which no user recovery is possible, declare a single user-visible exception. Inside the abstraction, provide a way to distinguish between the different internal errors.
- Do not borrow an exception name from another context.
- Export (declare visibly to the user) the names of all exceptions that can be raised.
- In a package, document which exceptions can be raised by each subprogram and task entry.
- Do not raise exceptions for internal errors that can be avoided or corrected within the unit.
- Do not raise the same exception to report different kinds of errors that are distinguishable by the user of the unit.
- Provide interrogative functions that allow the user of a unit to avoid causing exceptions to be raised.
- When possible, avoid changing state information in a unit before raising an exception.
- Catch and convert or handle all predefined and compiler-defined exceptions at the earliest opportunity.
- Do not explicitly raise predefined or implementation-defined exceptions.
- Never let an exception propagate beyond its scope.
example
This package specification defines two exceptions that enhance the abstraction:
-------------------------------------------------------------------------
generic
type Element is private;
package Stack is
function Stack_Empty return Boolean;
function Stack_Full return Boolean;
procedure Pop (From_Top : out Element);
procedure Push (Onto_Top : in Element);
-- Raised when Pop is used on empty stack.
Underflow : exception;
-- Raised when Push is used on full stack.
Overflow : exception;
end Stack;
-------------------------------------------------------------------------
...
----------------------------------------------------------------------
procedure Pop (From_Top : out Element) is
begin
...
if Stack_Empty then
raise Underflow;
else -- Stack contains at least one element
Top_Index := Top_Index - 1;
From_Top := Data(Top_Index + 1);
end if;
end Pop;
--------------------------------------------------------------------
...
rationale
Exceptions should be used as part of an abstraction to indicate error conditions that the abstraction is unable to prevent or correct. Because the abstraction is unable to correct such an error, it must report the error to the user. In the case of a usage error (e.g., attempting to invoke operations in the wrong sequence or attempting to exceed a boundary condition), the user may be able to correct the error. In the case of an error beyond the control of the user, the user may be able to work around the error if there are multiple mechanisms available to perform the desired operation. In other cases, the user may have to abandon use of the unit, dropping into a degraded mode of limited functionality. In any case, the user must be notified.
Exceptions are a good mechanism for reporting such errors because they provide an alternate flow of control for dealing with errors. This allows error-handling code to be kept separate from the code for normal processing. When an exception is raised, the current operation is aborted and control is transferred directly to the appropriate exception handler.
Several of the guidelines above exist to maximize the ability of the user to distinguish and correct different kinds of errors. Declaring new exception names, rather than raising exceptions declared in other packages, reduces the coupling between packages and also makes different exceptions more distinguishable. Exporting the names of all exceptions that a unit can raise, rather than declaring them internally to the unit, makes it possible for users of the unit to refer to the names in exception handlers. Otherwise, the user would be able to handle the exception only with an others handler. Finally, use comments to document exactly which of the exceptions declared in a package can be raised by each subprogram or task entry making it possible for the user to know which exception handlers are appropriate in each situation.
In situations where there are errors for which the abstraction user can take no intelligent action (e.g., there is no workaround or degraded mode), it is better to export a single internal error exception. Within the package, you should consider distinguishing between the different internal errors. For instance, you could record or handle different kinds of internal error in different ways. When you propagate the error to the user, however, you should use a special internal error exception, indicating that no user recovery is possible. You should also provide relevant information when you propagate the error, using the facilities provided in Ada.Exceptions. Thus, for any abstraction, you effectively provide N + 1 different exceptions: N different recoverable errors and one irrecoverable error for which there is no mapping to the abstraction. Both the application requirements and what the client needs/wants in terms of error information help you identify the appropriate exceptions for an abstraction.
Because they cause an immediate transfer of control, exceptions are useful for reporting unrecoverable errors, which prevent an operation from being completed, but not for reporting status or modes incidental to the completion of an operation. They should not be used to report internal errors that a unit was able to correct invisibly to the user.
To provide the user with maximum flexibility, it is a good idea to provide interrogative functions that the user can call to determine whether an exception would be raised if a subprogram or task entry were invoked. The function Stack_Empty in the above example is such a function. It indicates whether Underflow would be raised if Pop were called. Providing such functions makes it possible for the user to avoid triggering exceptions.
To support error recovery by its user, a unit should try to avoid changing state during an invocation that raises an exception. If a requested operation cannot be completely and correctly performed, then the unit should either detect this before changing any internal state information or should revert to the state at the time of the request. For example, after raising the exception Underflow, the stack package in the above example should remain in exactly the same state it was in when Pop was called. If it were to partially update its internal data structures for managing the stack, then future Push and Pop operations would not perform correctly. This is always desirable, but not always possible.
User-defined exceptions should be used instead of predefined or compiler-defined exceptions because they are more descriptive and more specific to the abstraction. The predefined exceptions are very general and can be triggered by many different situations. Compiler-defined exceptions are nonportable and have meanings that are subject to change even between successive releases of the same compiler. This introduces too much uncertainty for the creation of useful handlers.
If you are writing an abstraction, remember that the user does not know about the units you use in your implementation. That is an effect of information hiding. If any exception is raised within your abstraction, you must catch it and handle it. The user is not able to provide a reasonable handler if the original exception is allowed to propagate out of the body of your abstraction. You can still convert the exception into a form intelligible to the user if your abstraction cannot effectively recover on its own.
Converting an exception means raising a user-defined exception in the handler for the original exception. This introduces a meaningful name for export to the user of the unit. Once the error situation is couched in terms of the application, it can be handled in those terms.
Summary
high-level structure
- Place the specification of each library unit package in a separate file from its body.
- Avoid defining library unit subprograms that are not intended to be used as main programs. If such subprograms are defined, then create an explicit specification, in a separate file, for each library unit subprogram.
- Minimize the use of subunits.
- In preference to subunits, use child library units to structure a subsystem into manageable units.
- Place each subunit in a separate file.
- Use a consistent file naming convention.
- In preference to nesting in a package body, use a private child and with it to the parent body.
- Use private child unit specifications for data and subprograms that are required by (other) child units that extend a parent unit's abstraction or services.
- When possible, express configuration pragmas through compiler options or other means that do not require modifications to the source code. .
- When configuration pragmas must be placed in source code, consider isolating them to one compilation unit per partition; if specified, the main subprogram for the partition is recommended.
- Use subprograms to enhance abstraction.
- Restrict each subprogram to the performance of a single action.
- Use a function when the subprogram's primary purpose is to provide a single value.
- Minimize the side effect of a function.
- Consider using a parameterless function when the value does not need to be static.
- Use a parameterless function (instead of a constant) if the value should be inherited by types derived from the type.
- Use a parameterless function if the value itself is subject to change.
- Use packages for information hiding.
- Use packages with tagged types and private types for abstract data types.
- Use packages to model abstract entities appropriate to the problem domain.
- Use packages to group together related type and object declarations (e.g., common declarations for two or more library units).
- Encapsulate machine dependencies in packages. Place a software interface to a particular device in a package to facilitate a change to a different device.
- Place low-level implementation decisions or interfaces in subprograms within packages.
- Use packages and subprograms to encapsulate and hide program details that may change (Nissen and Wallis 1984).
- If a new library unit represents a logical extension to the original abstraction, define it as a child library unit.
- If a new library unit is independent (e.g., introduces a new abstraction that depends only in part on the existing one), then encapsulate the new abstraction in a separate library unit.
- Use child packages to implement a subsystem.
- Use public child units for those parts of a subsystem that should be visible to clients of the subsystem.
- Use private child units for those parts of a subsystem that should not be visible to clients of the subsystem.
- Use private child units for local declarations used only in implementing the package specification.
- Use child packages to implement constructors, even when they return access values.
- Make each package serve a single purpose.
- Use packages to group related data, types, and subprograms.
- Avoid collections of unrelated objects and subprograms (NASA 1987; Nissen and Wallis 1984).
- Consider restructuring a system to move two highly related units into the same package (or package hierarchy) or to move relatively independent units into separate packages.
- Avoid declaring variables in package specifications.
- Use tasks to model abstract, asynchronous entities within the problem domain.
- Use tasks to define concurrent algorithms for multiprocessor architectures.
- Use tasks to perform concurrent, cyclic, or prioritized activities (NASA 1987).
- Use protected types to control or synchronize access to data or devices.
- Use protected types to implement synchronization tasks, such as a passive resource monitor.
visibility
- Put only what is needed for the use of a package into its specification.
- Minimize the number of declarations in package specifications.
- Do not include extra operations simply because they are easy to build.
- Minimize the context (with) clauses in a package specification.
- Reconsider subprograms that seem to require large numbers of parameters.
- Do not manipulate global data within a subprogram or package merely to limit the number of parameters.
- Avoid unnecessary visibility; hide the implementation details of a program unit from its users.
- Use child library units to control the visibility of parts of a subsystem interface.
- Use private child packages for those declarations that should not be used outside the subsystem.
- Use child library units to present different views of an entity to different clients.
- Design (and redesign) interfaces after having worked out the logic of various expected clients of the interface.
- Use child packages rather than nested packages to present different views of the same abstraction.
- Nest package specifications within another package specification only for grouping operations or hiding common implementation details.
- Consider using private child packages in lieu of nesting.
- Restrict the visibility of program units as much as possible by nesting them inside package bodies (Nissen and Wallis 1984) if you cannot use a private child package.
- Minimize nesting program units inside subprograms and tasks.
- Minimize the scope within which with clauses apply.
- Only with those units directly needed.
- Carefully consider encapsulation of tasks.
exceptions
- For unavoidable internal errors for which no user recovery is possible, declare a single user-visible exception. Inside the abstraction, provide a way to distinguish between the different internal errors.
- Do not borrow an exception name from another context.
- Export (declare visibly to the user) the names of all exceptions that can be raised.
- In a package, document which exceptions can be raised by each subprogram and task entry.
- Do not raise exceptions for internal errors that can be avoided or corrected within the unit.
- Do not raise the same exception to report different kinds of errors that are distinguishable by the user of the unit.
- Provide interrogative functions that allow the user of a unit to avoid causing exceptions to be raised.
- When possible, avoid changing state information in a unit before raising an exception.
- Catch and convert or handle all predefined and compiler-defined exceptions at the earliest opportunity.
- Do not explicitly raise predefined or implementation-defined exceptions.
- Never let an exception propagate beyond its scope.
Programming Practices
Introduction
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 exitstatement.
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 acceptstatement.
- 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 [Annotated]).
Access-to-subprogram types are useful in implementing dynamic selection. References to the subprograms can be stored directly in the data structure. In a finite state machine, for example, a single data structure can describe the action to be taken on state transitions. Strong type checking is maintained because Ada 95 requires that the designated subprogram has the same parameter/result profile as the one specified in the subprogram access type.
See also Guideline 7.3.2 .
Data Structures
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 "free list." Keep track of which objects you are finished with, and reuse them instead of dynamically allocating new objects later.
The dangers of dangling references are that you may attempt to use them, thereby accessing memory that you have released to the memory manager and that may have been subsequently allocated for another purpose in another part of your program. When you read from such memory, unexpected errors may occur because the other part of your program may have previously written totally unrelated data there. Even worse, when you write to such memory you can cause errors in an apparently unrelated part of the code by changing values of variables dynamically allocated by that code. This type of error can be very difficult to find. Finally, such errors may be triggered in parts of your environment that you did not write, for example, in the memory management system itself, which may dynamically allocate memory to keep records about your dynamically allocated memory.
Keep in mind that any unreset component of a record or array can also be a dangling reference or carry a bit pattern representing inconsistent data. Components of an access type are always initialized by default to null; however, you should not rely on this default initialization. To enhance readability and maintainability, you should include explicit initialization.
Whenever you use dynamic allocation, it is possible to run out of space. Ada provides a facility (a length clause) for requesting the size of the pool of allocation space at compile time. Anticipate that you can still run out at run time. Prepare handlers for the exception Storage_Error, and consider carefully what alternatives you may be able to include in the program for each such situation.
There is a school of thought that dictates avoidance of all dynamic allocation. It is largely based on the fear of running out of memory during execution. Facilities, such as length clauses and exception handlers for Storage_Error, provide explicit control over memory partitioning and error recovery, making this fear unfounded.
When implementing a complex data structure (tree, list, sparse matrices, etc.), you often use access types. If you are not careful, you can consume all your storage with these dynamically allocated objects. You could export a deallocate operation, but it is impossible to ensure that it is called at the proper places; you are, in effect, trusting the clients. If you derive from controlled types (see 8.3.3 , and 9.2.3 for more information), you can use finalization to deal with deallocation of dynamic data, thus avoiding storage exhaustion. User-defined storage pools give better control over the allocation policy.
A related but distinct issue is that of shared versus copy semantics: even if the data structure is implemented using access types, you do not necessarily want shared semantics. In some instances you really want := to create a copy, not a new reference, and you really want = to compare the contents, not the reference. You should implement your structure as a controlled type. If you want copy semantics, you can redefine Adjust to perform a deep copy and = to perform a comparison on the contents. You can also redefine Finalize to make sure that when an object is finalized (i.e., goes out of scope) the dynamically allocated elements are chained on a free list (or deallocated by Ada.Unchecked_Deallocation).
The implicit use of dynamic (heap) storage by an Ada program during execution poses significant risks that software failures may occur. An Ada run-time environment may use implicit dynamic (heap) storage in association with composite objects, dynamically created tasks, and catenation. Often, the algorithms used to manage the dynamic allocation and reclamation of heap storage cause fragmentation or leakage, which can lead to storage exhaustion. It is usually very difficult or impossible to recover from storage exhaustion or Storage_Error without reloading and restarting the Ada program. It would be very restrictive to avoid all uses of implicit allocation. On the other hand, preventing both explicit and implicit deallocation significantly reduces the risks of fragmentation and leakage without overly restricting your use of composite objects, access values, task objects, and catenation.
exceptions
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 and or.
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 [Annotated] and follow the same commonly accepted precedence of algebraic operators. The strong typing facility in Ada combined with the common precedence rules make many parentheses unnecessary. However, when an uncommon combination of operators occurs, it may be helpful to add parentheses even when the precedence rules apply. The expression:
5 + ((Y ** 3) mod 10)
is clearer, and equivalent to:
5 + Y**3 mod 10
The rules of evaluation do specify left to right evaluation for operators with the same precedence level. However, it is the most commonly overlooked rule of evaluation when checking expressions for correctness.
Positive Forms of Logic
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 a case statement?
- Am I using else if where I could be using elsif ?
- Can I reorder the conditional expressions controlling this nested structure?
- Is there a different design that would be simpler?
exceptions
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 a case statement.
- Do not use ranges of enumeration literals in case statements.
- Use case statements rather than if/elsif statements, wherever possible.
- Use type extension and dispatching rather than case statements if, possible.
example
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'.
Loops
guideline
- Use for loops, whenever possible.
- Use while loops when the number of iterations cannot be calculated before entering the loop but a simple continuation condition can be applied at the top of the loop.
- Use plain loops with exit statements for more complex situations.
- Avoid exit statements in while and for loops.
- Minimize the number of ways to exit a loop.
example
To iterate over all elements of an array:
for I in Array_Name'Range loop
...
end loop;
To iterate over all elements in a linked list:
Pointer := Head_Of_List;
while Pointer /= null loop
...
Pointer := Pointer.Next;
end loop;
Situations requiring a "loop and a half" arise often. For this, use:
P_And_Q_Processing:
loop
P;
exit P_And_Q_Processing when Condition_Dependent_On_P;
Q;
end loop P_And_Q_Processing;
rather than:
P;
while not Condition_Dependent_On_P loop
Q;
P;
end loop;
rationale
A for loop is bounded, so it cannot be an "infinite loop." This is enforced by the Ada language, which requires a finite range in the loop specification and does not allow the loop counter of a for loop to be modified by a statement executed within the loop. This yields a certainty of understanding for the reader and the writer not associated with other forms of loops. A for loop is also easier to maintain because the iteration range can be expressed using attributes of the data structures upon which the loop operates, as shown in the example above where the range changes automatically whenever the declaration of the array is modified. For these reasons, it is best to use the for loop whenever possible, that is, whenever simple expressions can be used to describe the first and last values of the loop counter.
The while loop has become a very familiar construct to most programmers. At a glance, it indicates the condition under which the loop continues. Use the while loop whenever it is not possible to use the for loop but when there is a simple Boolean expression describing the conditions under which the loop should continue, as shown in the example above.
The plain loop statement should be used in more complex situations, even if it is possible to contrive a solution using a for or while loop in conjunction with extra flag variables or exit statements. The criteria in selecting a loop construct are to be as clear and maintainable as possible. It is a bad idea to use an exit statement from within a for or while loop because it is misleading to the reader after having apparently described the complete set of loop conditions at the top of the loop. A reader who encounters a plain loop statement expects to see exit statements.
There are some familiar looping situations that are best achieved with the plain loop statement. For example, the semantics of the Pascal repeat until loop, where the loop is always executed at least once before the termination test occurs, are best achieved by a plain loop with a single exit at the end of the loop. Another common situation is the "loop and a half" construct, shown in the example above, where a loop must terminate somewhere within the sequence of statements of the body. Complicated "loop and a half" constructs simulated with while loops often require the introduction of flag variables or duplication of code before and during the loop, as shown in the example. Such contortions make the code more complex and less reliable.
Minimize the number of ways to exit a loop to make the loop more understandable to the reader. It should be rare that you need more than two exit paths from a loop. When you do, be sure to use exit statements for all of them, rather than adding an exit statement to a for or while loop.
Exit Statements
guideline
- Use exit statements to enhance the readability of loop termination code (NASA 1987).
- Use exit when ... rather than if ... then exitwhenever possible (NASA 1987).
- Review exit statement placement.
example
See the examples in Guidelines 5.1.1 and Guidelines 5.6.4.
rationale
It is more readable to use exit statements than to try to add Boolean flags to a while loop condition to simulate exits from the middle of a loop. Even if all exit statements would be clustered at the top of the loop body, the separation of a complex condition into multiple exit statements can simplify and make it more readable and clear. The sequential execution of two exit statements is often more clear than the short-circuit control forms.
The exit when form is preferable to the if ... then exit form because it makes the word exit more visible by not nesting it inside of any control construct. The if ... then exit form is needed only in the case where other statements, in addition to the exit statement, must be executed conditionally. For example:
Process_Requests:
loop
if Status = Done then
Shut_Down;
exit Process_Requests;
end if;
...
end loop Process_Requests;
Loops with many scattered exit statements can indicate fuzzy thinking regarding the loop's purpose in the algorithm. Such an algorithm might be coded better some other way, for example, with a series of loops. Some rework can often reduce the number of exit statements and make the code clearer.
See also Guidelines 5.1.3 and 5.6.4.
Recursion and Iteration Bounds
guideline
- Consider specifying bounds on loops .
- Consider specifying bounds on recursion .
example
Establishing an iteration bound:
Safety_Counter := 0;
Process_List:
loop
exit when Current_Item = null;
...
Current_Item := Current_Item.Next;
...
Safety_Counter := Safety_Counter + 1;
if Safety_Counter > 1_000_000 then
raise Safety_Error;
end if;
end loop Process_List;
Establishing a recursion bound:
subtype Recursion_Bound is Natural range 0 .. 1_000;
procedure Depth_First (Root : in Tree;
Safety_Counter : in Recursion_Bound
:= Recursion_Bound'Last) is
begin
if Root /= null then
if Safety_Counter = 0 then
raise Recursion_Error;
end if;
Depth_First (Root => Root.Left_Branch,
Safety_Counter => Safety_Counter - 1);
Depth_First (Root => Root.Right_Branch,
Safety_Counter => Safety_Counter - 1);
... -- normal subprogram body
end if;
end Depth_First;
Following are examples of this subprogram's usage. One call specifies a maximum recursion depth of 50. The second takes the default (1,000). The third uses a computed bound:
Depth_First(Root => Tree_1, Safety_Counter => 50);
Depth_First(Tree_2);
Depth_First(Root => Tree_3, Safety_Counter => Current_Tree_Height);
rationale
Recursion, and iteration using structures other than for statements, can be infinite because the expected terminating condition does not arise. Such faults are sometimes quite subtle, may occur rarely, and may be difficult to detect because an external manifestation might be absent or substantially delayed.
By including counters and checks on the counter values, in addition to the loops themselves, you can prevent many forms of infinite loops. The inclusion of such checks is one aspect of the technique called Safe Programming (Anderson and Witty 1978).
The bounds of these checks do not have to be exact, just realistic. Such counters and checks are not part of the primary control structure of the program but a benign addition functioning as an execution-time "safety net," allowing error detection and possibly recovery from potential infinite loops or infinite recursion.
notes
If a loop uses the for iteration scheme (Guideline 5.6.4), it follows this guideline.
exceptions
Embedded control applications have loops that are intended to be infinite. Only a few loops within such applications should qualify as exceptions to this guideline. The exceptions should be deliberate (and documented ) policy decisions.
This guideline is most important to safety critical systems. For other systems, it may be overkill.
Goto Statements
guideline
Do not use goto statements.
rationale
A goto statement is an unstructured change in the control flow. Worse, the label does not require an indicator of where the corresponding goto statement(s) are. This makes code unreadable and makes its correct execution suspect.
Other languages use goto statements to implement loop exits and exception handling. Ada's support of these constructs makes the goto statement extremely rare.
notes
If you should ever use a goto statement, highlight both it and the label with blank space. Indicate at the label where the corresponding goto statement(s) may be found.
Return Statements
guideline
- Minimize the number of return statements from a subprogram (NASA 1987).
- Highlight return statements with comments or white space to keep them from being lost in other code.
example
The following code fragment is longer and more complex than necessary:
if Pointer /= null then
if Pointer.Count > 0 then
return True;
else -- Pointer.Count = 0
return False;
end if;
else -- Pointer = null
return False;
end if;
It should be replaced with the shorter, more concise, and clearer equivalent line:
return Pointer /= null and then Pointer.Count > 0;
rationale
Excessive use of returns can make code confusing and unreadable. Only use return statements where warranted. Too many returns from a subprogram may be an indicator of cluttered logic. If the application requires multiple returns, use them at the same level (i.e., as in different branches of a case statement), rather than scattered throughout the subprogram code. Some rework can often reduce the number of return statements to one and make the code more clear.
exceptions
Do not avoid return statements if it detracts from natural structure and code readability.
Blocks
guideline
- Use blocks to localize the scope of declarations.
- Use blocks to perform local renaming.
- Use blocks to define local exception handlers.
example
with Motion;
with Accelerometer_Device;
...
---------------------------------------------------------------------
function Maximum_Velocity return Motion.Velocity is
Cumulative : Motion.Velocity := 0.0;
begin -- Maximum_Velocity
-- Initialize the needed devices
...
Calculate_Velocity_From_Sample_Data:
declare
use type Motion.Acceleration;
Current : Motion.Acceleration := 0.0;
Time_Delta : Duration;
begin -- Calculate_Velocity_From_Sample_Data
for I in 1 .. Accelerometer_Device.Sample_Limit loop
Get_Samples_And_Ignore_Invalid_Data:
begin
Accelerometer_Device.Get_Value(Current, Time_Delta);
exception
when Constraint_Error =>
null; -- Continue trying
when Accelerometer_Device.Failure =>
raise Accelerometer_Device_Failed;
end Get_Samples_And_Ignore_Invalid_Data;
exit when Current <= 0.0; -- Slowing down
Update_Velocity:
declare
use type Motion.Velocity;
use type Motion.Acceleration;
begin
Cumulative := Cumulative + Current * Time_Delta;
exception
when Constraint_Error =>
raise Maximum_Velocity_Exceeded;
end Update_Velocity;
end loop;
end Calculate_Velocity_From_Sample_Data;
return Cumulative;
end Maximum_Velocity;
---------------------------------------------------------------------
...
rationale
Blocks break up large segments of code and isolate details relevant to each subsection of code. Variables that are only used in a particular section of code are clearly visible when a declarative block delineates that code.
Renaming may simplify the expression of algorithms and enhance readability for a given section of code. But it is confusing when a renames clause is visually separated from the code to which it applies. The declarative region allows the renames to be immediately visible when the reader is examining code that uses that abbreviation. Guideline 5.7.1 discusses a similar guideline concerning the use clause.
Local exception handlers can catch exceptions close to the point of origin and allow them to be either handled, propagated, or converted.
Aggregates
guideline
- Use an aggregate instead of a sequence of assignments to assign values to all components of a record.
- Use an aggregate instead of a temporary variable when building a record to pass as an actual parameter.
- Use positional association only when there is a conventional ordering of the arguments.
example
It is better to use aggregates:
Set_Position((X, Y));
Employee_Record := (Number => 42,
Age => 51,
Department => Software_Engineering);
than to use consecutive assignments or temporary variables:
Temporary_Position.X := 100;
Temporary_Position.Y := 200;
Set_Position(Temporary_Position);
Employee_Record.Number := 42;
Employee_Record.Age := 51;
Employee_Record.Department := Software_Engineering;
rationale
Using aggregates during maintenance is beneficial. If a record structure is altered, but the corresponding aggregate is not, the compiler flags the missing field in the aggregate assignment. It would not be able to detect the fact that a new assignment statement should have been added to a list of assignment statements.
Aggregates can also be a real convenience in combining data items into a record or array structure required for passing the information as a parameter. Named component association makes aggregates more readable.
See Guideline 10.4.5 for the performance impact of aggregates.
Visibility
As noted in Guideline 4.2, Ada's ability to enforce information hiding and separation of concerns through its visibility controlling features is one of the most important advantages of the language. Subverting these features, for example, by too liberal use of the use clause, is wasteful and dangerous.
The Use Clause
guideline
- When you need to provide visibility to operators, use the use type clause.
- Avoid/minimize the use of the use clause (Nissen and Wallis 1984).
- Consider using a package renames clause rather than a use clause for a package.
- Consider using the use clause in the following situations:
- When standard packages are needed and no ambiguous references are introduced
- When references to enumeration literals are needed
- Localize the effect of all use clauses.
example
This is a modification of the example from Guideline 4.2.3. The effect of a use clause is localized:
----------------------------------------------------------------------------------
package Rational_Numbers is
type Rational is private;
function "=" (X, Y : Rational) return Boolean;
function "/" (X, Y : Integer) return Rational; -- construct a rational number
function "+" (X, Y : Rational) return Rational;
function "-" (X, Y : Rational) return Rational;
function "*" (X, Y : Rational) return Rational;
function "/" (X, Y : Rational) return Rational; -- rational division
private
...
end Rational_Numbers;
----------------------------------------------------------------------------------
package body Rational_Numbers is
procedure Reduce (R : in out Rational) is . . . end Reduce;
. . .
end Rational_Numbers;
----------------------------------------------------------------------------------
package Rational_Numbers.IO is
...
procedure Put (R : in Rational);
procedure Get (R : out Rational);
end Rational_Numbers.IO;
----------------------------------------------------------------------------------
with Rational_Numbers;
with Rational_Numbers.IO;
with Ada.Text_IO;
procedure Demo_Rationals is
package R_IO renames Rational_Numbers.IO;
use type Rational_Numbers.Rational;
use R_IO;
use Ada.Text_IO;
X : Rational_Numbers.Rational;
Y : Rational_Numbers.Rational;
begin -- Demo_Rationals
Put ("Please input two rational numbers: ");
Get (X);
Skip_Line;
Get (Y);
Skip_Line;
Put ("X / Y = ");
Put (X / Y);
New_Line;
Put ("X * Y = ");
Put (X * Y);
New_Line;
Put ("X + Y = ");
Put (X + Y);
New_Line;
Put ("X - Y = ");
Put (X - Y);
New_Line;
end Demo_Rationals;
rationale
These guidelines allow you to maintain a careful balance between maintainability and readability. Use of the use clause may indeed make the code read more like prose text. However, the maintainer may also need to resolve references and identify ambiguous operations. In the absence of tools to resolve these references and identify the impact of changing use clauses, fully qualified names are the best alternative.
Avoiding the use clause forces you to use fully qualified names. In large systems, there may be many library units named in with clauses. When corresponding use clauses accompany the with clauses and the simple names of the library packages are omitted (as is allowed by the use clause), references to external entities are obscured and identification of external dependencies becomes difficult.
In some situations, the benefits of the use clause are clear. A standard package can be used with the obvious assumption that the reader is very familiar with those packages and that additional overloading will not be introduced.
The use type clause makes both infix and prefix operators visible without the need for renames clauses. You enhance readability with the use type clause because you can write statements using the more natural infix notation for operators. See also Guideline 5.7.2.
You can minimize the scope of the use clause by placing it in the body of a package or subprogram or by encapsulating it in a block to restrict visibility.
notes
Avoiding the use clause completely can cause problems with enumeration literals, which must then be fully qualified. This problem can be solved by declaring constants with the enumeration literals as their values, except that such constants cannot be overloaded like enumeration literals.
An argument defending the use clause can be found in Rosen (1987).
automation notes
There are tools that can analyze your Ada source code, resolve overloading of names, and automatically convert between the use clause or fully qualified names.
The Renames Clause
guideline
- Limit the scope of a renaming declaration to the minimum necessary scope.
- Rename a long, fully qualified name to reduce the complexity if it becomes unwieldy (see Guideline 3.1.4).
- Use renaming to provide the body of a subprogram if this subprogram merely calls the first subprogram.
- Rename declarations for visibility purposes rather than using the use clause, except for operators (see Guideline 5.7.1).
- Rename parts when your code interfaces to reusable components originally written with nondescriptive or inapplicable nomenclature.
- Use a project-wide standard list of abbreviations to rename common packages.
- Provide a use type rather than a renames clause to provide visibility to operators.
example
procedure Disk_Write (Track_Name : in Track;
Item : in Data) renames
System_Specific.Device_Drivers.Disk_Head_Scheduler.Transmit;
See also the example in Guideline 5.7.1, where a package-level renames clause provides an abbreviation for the package Rational_Numbers_IO.
rationale
If the renaming facility is abused, the code can be difficult to read. A renames clause can substitute an abbreviation for a qualifier or long package name locally. This can make code more readable yet anchor the code to the full name. You can use the renames clause to evaluate a complex name once or to provide a new "view" of an object (regardless of whether it is tagged). However, the use of renames clauses can often be avoided or made obviously undesirable by carefully choosing names so that fully qualified names read well.
When a subprogram body calls another subprogram without adding local data or other algorithmic content, it is more readable to have this subprogram body rename the subprogram that actually does the work. Thus, you avoid having to write code to "pass through" a subprogram call (Rationale 1995, §II.12).
The list of renaming declarations serves as a list of abbreviation definitions (see Guideline 3.1.4). As an alternative, you can rename a package at the library level to define project-wide abbreviations for packages and then with the renamed packages. Often the parts recalled from a reuse library do not have names that are as general as they could be or that match the new application's naming scheme. An interface package exporting the renamed subprograms can map to your project's nomenclature. See also Guideline 5.7.1.
The method described in the Ada Reference Manual 1995, §8.5 [Annotated] for renaming a type is to use a subtype (see Guideline 3.4.1).
The use type clause eliminates the need for renaming infix operators. Because you no longer need to rename each operator explicitly, you avoid errors such as renaming a + to a -. See also Guideline 5.7.1.
notes
You should choose package names to be minimally meaningful, recognizing that package names will be widely used as prefixes (e.g., Pkg.Operation or Object : Pkg.Type_Name;). If you rename every package to some abbreviation, you defeat the purpose of choosing meaningful names, and it becomes hard to keep track of what all the abbreviations represent.
For upward compatibility of Ada 83 programs in an Ada 95 environment, the environment includes library-level renamings of the Ada 83 library level packages (Ada Reference Manual 1995, §J.1 [Annotated]). It is not recommended that you use these renamings in Ada 95 code.
Overloaded Subprograms
guideline
Limit overloading to widely used subprograms that perform similar actions on arguments of different types (Nissen and Wallis 1984).
example
function Sin (Angles : in Matrix_Of_Radians) return Matrix;
function Sin (Angles : in Vector_Of_Radians) return Vector;
function Sin (Angle : in Radians) return Small_Real;
function Sin (Angle : in Degrees) return Small_Real;
rationale
Excessive overloading can be confusing to maintainers (Nissen and Wallis 1984, 65). There is also the danger of hiding declarations if overloading becomes habitual. Attempts to overload an operation may actually hide the original operation if the parameter profile is not distinct. From that point on, it is not clear whether invoking the new operation is what the programmer intended or whether the programmer intended to invoke the hidden operation and accidentally hid it.
notes
This guideline does not prohibit subprograms with identical names declared in different packages.
Overloaded Operators
guideline
- Preserve the conventional meaning of overloaded operators (Nissen and Wallis 1984).
- Use "+" to identify adding, joining, increasing, and enhancing kinds of functions.
- Use "-" to identify subtraction, separation, decreasing, and depleting kinds of functions.
- Use operator overloading sparingly and uniformly when applied to tagged types.
example
function "+" (X : in Matrix;
Y : in Matrix)
return Matrix;
...
Sum := A + B;
rationale
Subverting the conventional interpretation of operators leads to confusing code.
The advantage of operator overloading is that the code can become more clear and written more compactly (and readably) when it is used. This can make the semantics simple and natural. However, it can be easy to misunderstand the meaning of an overloaded operator, especially when applied to descendants. This is especially true if the programmer has not applied natural semantics. Thus, do not use overloading if it cannot be used uniformly and if it is easily misunderstood.
notes
There are potential problems with any overloading. For example, if there are several versions of the "+" operator and a change to one of them affects the number or order of its parameters, locating the occurrences that must be changed can be difficult.
Overloading the Equality Operator
guideline
- Define an appropriate equality operator for private types.
- Consider redefining the equality operator for a private type.
- When overloading the equality operator for types, maintain the properties of an algebraic equivalence relation.
rationale
The predefined equality operation provided with private types depends on the data structure chosen to implement that type . If access types are used, then equality will mean the operands have the same pointer value. If discrete types are used, then equality will mean the operands have the same value. If a floating- point type is used, then equality is based on Ada model intervals (see Guideline 7.2.7). You should, therefore, redefine equality to provide the meaning expected by the client. If you implement a private type using an access type, you should redefine equality to provide a deep equality. For floating-point types, you may want to provide an equality that tests for equality within some application-dependent epsilon value.
Any assumptions about the meaning of equality for private types will create a dependency on the implementation of that type. See Gonzalez (1991) for a detailed discussion.
When the definition of "=" is provided, there is a conventional algebraic meaning implied by this symbol. As described in Baker (1991), the following properties should remain true for the equality operator:
- Reflexive: a = a
- Symmetric: a = b ==> b = a
- Transitive: a = b and b = c ==> a = c
In redefining equality, you are not required to have a result type of Standard.Boolean. The Rationale (1995, §6.3) gives two examples where your result type is a user-defined type. In a three-valued logic abstraction, you redefine equality to return one of True, False, or Unknown. In a vector processing application, you can define a component-wise equality operator that returns a vector of Boolean values. In both these instances, you should also redefine inequality because it is not the Boolean complement of the equality function.
Using exceptions
Ada exceptions are a reliability-enhancing language feature designed to help specify program behavior in the presence of errors or unexpected events. Exceptions are not intended to provide a general purpose control construct. Further, liberal use of exceptions should not be considered sufficient for providing full software fault tolerance (Melliar-Smith and Randell 1987).
This section addresses the issues of how and when to avoid raising exceptions, how and where to handle them, and whether to propagate them. Information on how to use exceptions as part of the interface to a unit includes what exceptions to declare and raise and under what conditions to raise them. Other issues are addressed in the guidelines in Sections 4.3 and 7.5.
Handling Versus Avoiding Exceptions
guideline
- When it is easy and efficient to do so, avoid causing exceptions to be raised.
- Provide handlers for exceptions that cannot be avoided.
- Use exception handlers to enhance readability by separating fault handling from normal execution.
- Do not use exceptions and exception handlers as goto statements.
- Do not evaluate the value of an object (or a part of an object) that has become abnormal because of the failure of a language-defined check.
rationale
In many cases, it is possible to detect easily and efficiently that an operation you are about to perform would raise an exception. In such a case, it is a good idea to check rather than allowing the exception to be raised and handling it with an exception handler. For example, check each pointer for null when traversing a linked list of records connected by pointers. Also, test an integer for 0 before dividing by it, and call an interrogative function Stack_Is_Empty before invoking the pop procedure of a stack package. Such tests are appropriate when they can be performed easily and efficiently as a natural part of the algorithm being implemented.
However, error detection in advance is not always so simple. There are cases where such a test is too expensive or too unreliable. In such cases, it is better to attempt the operation within the scope of an exception handler so that the exception is handled if it is raised. For example, in the case of a linked list implementation of a list, it is very inefficient to call a function Entry_Exists before each call to the procedure Modify_Entry simply to avoid raising the exception Entry_Not_Found. It takes as much time to search the list to avoid the exception as it takes to search the list to perform the update. Similarly, it is much easier to attempt a division by a real number within the scope of an exception handler to handle numeric overflow than to test, in advance, whether the dividend is too large or the divisor too small for the quotient to be representable on the machine.
In concurrent situations, tests done in advance can also be unreliable. For example, if you want to modify an existing file on a multiuser system, it is safer to attempt to do so within the scope of an exception handler than to test in advance whether the file exists, whether it is protected, whether there is room in the file system for the file to be enlarged, etc. Even if you tested for all possible error conditions, there is no guarantee that nothing would change after the test and before the modification operation. You still need the exception handlers, so the advance testing serves no purpose.
Whenever such a case does not apply, normal and predictable events should be handled by the code without the abnormal transfer of control represented by an exception. When fault handling and only fault handling code is included in exception handlers, the separation makes the code easier to read. The reader can skip all the exception handlers and still understand the normal flow of control of the code. For this reason, exceptions should never be raised and handled within the same unit, as a form of a goto statement to exit from a loop, if, case, or block statement.
Evaluating an abnormal object results in erroneous execution (Ada Reference Manual 1995, §13.9.1 [Annotated]). The failure of a language-defined check raises an exception. In the corresponding exception handler, you want to perform appropriate cleanup actions, including logging the error (see the discussion on exception occurrences in Guideline 5.8.2) and/or reraising the exception. Evaluating the object that put you into the exception handling code will lead to erroneous execution, where you do not know whether your exception handler has executed completely or correctly. See also Guideline 5.9.1, which discusses abnormal objects in the context of Ada.Unchecked_Conversion.
Handlers for Others
guideline
- When writing an exception handler for others, capture and return additional information about the exception through the Exception_Name, Exception_Message, or Exception_Information subprograms declared in the predefined package Ada.Exceptions.
- Use others only to catch exceptions you cannot enumerate explicitly, preferably only to flag a potential abort.
- During development, trap others, capture the exception being handled, and consider adding an explicit handler for that exception.
example
The following simplified example gives the user one chance to enter an integer in the range 1 to 3. In the event of an error, it provides information back to the user. For an integer value that is outside the expected range, the function reports the name of the exception. For any other error, the function provides more complete traceback information. The amount of traceback information is implementation dependent.
with Ada.Exceptions;
with Ada.Text_IO;
with Ada.Integer_Text_IO;
function Valid_Choice return Positive is
subtype Choice_Range is Positive range 1..3;
Choice : Choice_Range;
begin
Ada.Text_IO.Put ("Please enter your choice: 1, 2, or 3: ");
Ada.Integer_Text_IO.Get (Choice);
if Choice in Choice_Range then -- else garbage returned
return Choice;
end if;
when Out_of_Bounds : Constraint_Error =>
Ada.Text_IO.Put_Line ("Input choice not in range.");
Ada.Text_IO.Put_Line (Ada.Exceptions.Exception_Name (Out_of_Bounds));
Ada.Text_IO.Skip_Line;
when The_Error : others =>
Ada.Text_IO.Put_Line ("Unexpected error.");
Ada.Text_IO.Put_Line (Ada.Exceptions.Exception_Information (The_Error));
Ada.Text_IO.Skip_Line;
end Valid_Choice;
rationale
The predefined package Ada.Exceptions allows you to log an exception, including its name and traceback information. When writing a handler for others, you should provide information about the exception to facilitate debugging. Because you can access information about an exception occurrence, you can save information suitable for later analysis in a standard way. By using exception occurrences, you can identify the particular exception and either log the details or take corrective action.
Providing a handler for others allows you to follow the other guidelines in this section. It affords a place to catch and convert truly unexpected exceptions that were not caught by the explicit handlers. While it may be possible to provide "fire walls" against unexpected exceptions being propagated without providing handlers in every block, you can convert the unexpected exceptions as soon as they arise. The others handler cannot discriminate between different exceptions, and, as a result, any such handler must treat the exception as a disaster. Even such a disaster can still be converted into a user-defined exception at that point. Because a handler for others catches any exception not otherwise handled explicitly, one placed in the frame of a task or of the main subprogram affords the opportunity to perform final cleanup and to shut down cleanly.
Programming a handler for others requires caution. You should name it in the handler (e.g., Error : others;) to discriminate either which exception was actually raised or precisely where it was raised. In general, the others handler cannot make any assumptions about what can be or even what needs to be "fixed."
The use of handlers for others during development, when exception occurrences can be expected to be frequent, can hinder debugging unless you take advantage of the facilities in Ada.Exceptions. It is much more informative to the developer to see a traceback with the actual exception information as captured by the Ada.Exceptions subprograms. Writing a handler without these subprograms limits the amount of error information you may see. For example, you may only see the converted exception in a traceback that does not list the point where the original exception was raised.
notes
It is possible, but not recommended, to use Exception_Id to distinguish between different exceptions in an others handler. The type Exception_Id is implementation defined. Manipulating values of type Exception_Id reduces the portability of your program and makes it harder to understand.
Propagation
guideline
- Handle all exceptions, both user and predefined .
- For every exception that might be raised, provide a handler in suitable frames to protect against undesired propagation outside the abstraction .
rationale
The statement that "it can never happen" is not an acceptable programming approach. You must assume it can happen and be in control when it does. You should provide defensive code routines for the "cannot get here" conditions.
Some existing advice calls for catching and propagating any exception to the calling unit. This advice can stop a program. You should catch the exception and propagate it or a substitute only if your handler is at the wrong abstraction level to effect recovery. Effecting recovery can be difficult, but the alternative is a program that does not meet its specification.
Making an explicit request for termination implies that your code is in control of the situation and has determined that to be the only safe course of action. Being in control affords opportunities to shut down in a controlled manner (clean up loose ends, close files, release surfaces to manual control, sound alarms) and implies that all available programmed attempts at recovery have been made.
Localizing the Cause of an Exception
guideline
- Do not rely on being able to identify the fault-raising, predefined, or implementation-defined exceptions.
- Use the facilities defined in Ada.Exceptions to capture as much information as possible about an exception.
- Use blocks to associate localized sections of code with their own exception handlers.
example
See Guideline 5.6.9.
rationale
In an exception handler, it is very difficult to determine exactly which statement and which operation within that statement raised an exception, particularly the predefined and implementation-defined exceptions. The predefined and implementation-defined exceptions are candidates for conversion and propagation to higher abstraction levels for handling there. User-defined exceptions, being more closely associated with the application, are better candidates for recovery within handlers.
User-defined exceptions can also be difficult to localize. Associating handlers with small blocks of code helps to narrow the possibilities, making it easier to program recovery actions. The placement of handlers in small blocks within a subprogram or task body also allows resumption of the subprogram or task after the recovery actions. If you do not handle exceptions within blocks, the only action available to the handlers is to shut down the task or subprogram as prescribed in Guideline 5.8.3.
As discussed in Guideline 5.8.2, you can log run-time system information about the exception. You can also attach a message to the exception. During code development, debugging, and maintenance, this information should be useful to localize the cause of the exception.
notes
The optimal size for the sections of code you choose to protect by a block and its exception handlers is very application-dependent. Too small a granularity forces you to expend more effort in programming for abnormal actions than for the normal algorithm. Too large a granularity reintroduces the problems of determining what went wrong and of resuming normal flow.
Erroneous execution and bounded errors
Ada 95 introduces the category of bounded errors. Bounded errors are cases where the behavior is not deterministic but falls within well-defined bounds (Rationale 1995, §1.4). The consequence of a bounded error is to limit the behavior of compilers so that an Ada environment is not free to do whatever it wants in the presence of errors. The Ada Reference Manual 1995, §1.1.5 [Annotated] defines a set of possible outcomes for the consequences of undefined behavior, as in an uninitialized value or a value outside the range of its subtype. For example, the executing program may raise the predefined exception Program_Error, Constraint_Error, or it may do nothing.
An Ada program is erroneous when it generates an error that is not required to be detected by the compiler or run-time environments. As stated in the Ada Reference Manual 1995, §1.1.5 [Annotated], "The effects of erroneous execution are unpredictable." If the compiler does detect an instance of an erroneous program, its options are to indicate a compile time error; to insert the code to raise Program_Error , possibly to write a message to that effect; or to do nothing at all.
Erroneousness is not a concept unique to Ada. The guidelines below describe or explain some specific instances of erroneousness defined in the Ada Reference Manual 1995, §1.1.5 [Annotated]. These guidelines are not intended to be all-inclusive but rather emphasize some commonly overlooked problem areas. Arbitrary order dependencies are not, strictly speaking, a case of erroneous execution; thus, they are discussed in Guideline 7.1.9 as a portability issue.
Unchecked Conversion
guideline
- Use Ada.Unchecked_Conversion only with the utmost care (Ada Reference Manual 1995, §13.9 [Annotated]).
- Consider using the 'Valid attribute to check the validity of scalar data.
- Ensure that the value resulting from Ada.Unchecked_Conversion properly represents a value of the parameter's subtype.
- Isolate the use of Ada.Unchecked_Conversion in package bodies.
example
The following example shows how to use the 'Valid attribute to check validity of scalar data:
------------------------------------------------------------------------
with Ada.Unchecked_Conversion;
with Ada.Text_IO;
with Ada.Integer_Text_IO;
procedure Test is
type Color is (Red, Yellow, Blue);
for Color'Size use Integer'Size;
function Integer_To_Color is
new Ada.Unchecked_Conversion (Source => Integer,
Target => Color);
Possible_Color : Color;
Number : Integer;
begin -- Test
Ada.Integer_Text_IO.Get (Number);
Possible_Color := Integer_To_Color (Number);
if Possible_Color'Valid then
Ada.Text_IO.Put_Line(Color'Image(Possible_Color));
else
Ada.Text_IO.Put_Line("Number does not correspond to a color.");
end if;
end Test;
------------------------------------------------------------------------
rationale
An unchecked conversion is a bit-for-bit copy without regard to the meanings attached to those bits and bit positions by either the source or the destination type. The source bit pattern can easily be meaningless in the context of the destination type. Unchecked conversions can create values that violate type constraints on subsequent operations. Unchecked conversion of objects mismatched in size has implementation-dependent results.
Using the 'Valid attribute on scalar data allows you to check whether it is in range without raising an exception if it is out of range. There are several cases where such a validity check enhances the readability and maintainability of the code:
- Data produced through an unchecked conversion
- Input data
- Parameter values returned from a foreign language interface
- Aborted assignment (during asynchronous transfer of control or execution of an abort statement)
- Disrupted assignment from failure of a language-defined check
- Data whose address has been specified with the 'Address attribute
An access value should not be assumed to be correct when obtained without compiler or run-time checks. When dealing with access values, use of the 'Valid attribute helps prevent the erroneous dereferencing that might occur after using Ada.Unchecked_Deallocation, Unchecked_Access, or Ada.Unchecked_Conversion.
In the case of a nonscalar object used as an actual parameter in an unchecked conversion, you should ensure that its value on return from the procedure properly represents a value in the subtype. This case occurs when the parameter is of mode out or in out. It is important to check the value when interfacing to foreign languages or using a language-defined input procedure. The Ada Reference Manual 1995, §13.9.1 [Annotated] lists the full rules concerning data validity.
Unchecked Deallocation
guideline
- Isolate the use of Ada.Unchecked_Deallocation in package bodies.
- Ensure that no dangling reference to the local object exists after exiting the scope of the local object.
rationale
Most of the reasons for using Ada.Unchecked_Deallocation with caution have been given in Guideline 5.4.5. When this feature is used, no checking is performed to verify that there is only one access path to the storage being deallocated. Thus, any other access paths are not made null. Depending on the value of these other access paths could result in erroneous execution.
If your Ada environment implicitly uses dynamic heap storage but does not fully and reliably reclaim and reuse heap storage, you should not use Ada.Unchecked_Deallocation.
Unchecked Access
guideline
- Minimize the use of the attribute Unchecked_Access, preferably isolating it to package bodies.
- Use the attribute Unchecked_Access only on data whose lifetime/scope is "library level."
rationale
The accessibility rules are checked statically at compile time (except for access parameters, which are checked dynamically). These rules ensure that the access value cannot outlive the object it designates. Because these rules are not applied in the case of Unchecked_Access, an access path could be followed to an object no longer in scope.
Isolating the use of the attribute Unchecked_Access means to isolate its use from clients of the package. You should not apply it to an access value merely for the sake of returning a now unsafe value to clients.
When you use the attribute Unchecked_Access, you are creating access values in an unsafe manner. You run the risk of dangling references, which in turn lead to erroneous execution (Ada Reference Manual 1995, §13.9.1 [Annotated]).
exceptions
The Ada Reference Manual 1995, §13.10 [Annotated]) defines the following potential use for this otherwise dangerous attribute. "This attribute is provided to support the situation where a local object is to be inserted into a global linked data structure, when the programmer knows that it will always be removed from the data structure prior to exiting the object's scope."
Address Clauses
guideline
- Use address clauses to map variables and entries to the hardware device or memory, not to model the FORTRAN "equivalence" feature.
- Ensure that the address specified in an attribute definition clause is valid and does not conflict with the alignment.
- If available in your Ada environment, use the package Ada.Interrupts to associate handlers with interrupts.
- Avoid using the address clause for nonimported program units.
example
Single_Address : constant System.Address := System.Storage_Elements.To_Address(...);
Interrupt_Vector_Table : Hardware_Array;
for Interrupt_Vector_Table'Address use Single_Address;
rationale
The result of specifying a single address for multiple objects or program units is undefined, as is specifying multiple addresses for a single object or program unit. Specifying multiple address clauses for an interrupt is also undefined. It does not necessarily overlay objects or program units, or associate a single entry with more than one interrupt.
You are responsible for ensuring the validity of an address you specify. Ada requires that the object of an address be an integral multiple of its alignment.
In Ada 83 (Ada Reference Manual 1983) you had to use values of type System.Address to attach an interrupt entry to an interrupt. While this technique is allowed in Ada 95, you are using an obsolete feature. You should use a protected procedure and the appropriate pragmas (Rationale 1995, §C.3.2).
Suppression of Exception Check
guideline
- Do not suppress exception checks during development.
- If necessary, during operation, introduce blocks that encompass the smallest range of statements that can safely have exception checking removed.
rationale
If you disable exception checks and program execution results in a condition in which an exception would otherwise occur, the program execution is erroneous. The results are unpredictable. Further, you must still be prepared to deal with the suppressed exceptions if they are raised in and propagated from the bodies of subprograms, tasks, and packages you call.
By minimizing the code that has exception checking removed, you increase the reliability of the program. There is a rule of thumb that suggests that 20% of the code is responsible for 80% of the CPU time. So, once you have identified the code that actually needs exception checking removed, it is wise to isolate it in a block (with appropriate comments) and leave the surrounding code with exception checking in effect.
Initialization
guideline
- Initialize all objects prior to use.
- Use caution when initializing access values.
- Do not depend on default initialization that is not part of the language.
- Derive from a controlled type and override the primitive procedure to ensure automatic initialization.
- Ensure elaboration of an entity before using it.
- Use function calls in declarations cautiously.
example
The first example illustrates the potential problem with initializing access values:
procedure Mix_Letters (Of_String : in out String) is
type String_Ptr is access String;
Ptr : String_Ptr := new String'(Of_String); -- could raise Storage_Error in caller
begin -- Mix_Letters
...
exception
... -- cannot trap Storage_Error raised during elaboration of Ptr declaration
end Mix_Letters;
The second example illustrates the issue of ensuring the elaboration of an entity before its use:
------------------------------------------------------------------------
package Robot_Controller is
...
function Sense return Position;
...
end Robot_Controller;
------------------------------------------------------------------------
package body Robot_Controller is
...
Goal : Position := Sense; -- This raises Program_Error
...
---------------------------------------------------------------------
function Sense return Position is
begin
...
end Sense;
---------------------------------------------------------------------
begin -- Robot_Controller
Goal := Sense; -- The function has been elaborated.
...
end Robot_Controller;
------------------------------------------------------------------------
rationale
Ada does not define an initial default value for objects of any type other than access types, whose initial default value is null. If you are initializing an access value at the point at which it is declared and the allocation raises the exception Storage_Error, the exception is raised in the calling not the called procedure. The caller is unprepared to handle this exception because it knows nothing about the problem-causing allocation.
Operating systems differ in what they do when they allocate a page in memory: one operating system may zero out the entire page; a second may do nothing. Therefore, using the value of an object before it has been assigned a value causes unpredictable (but bounded) behavior, possibly raising an exception. Objects can be initialized implicitly by declaration or explicitly by assignment statements. Initialization at the point of declaration is safest as well as easiest for maintainers. You can also specify default values for components of records as part of the type declarations for those records.
Ensuring initialization does not imply initialization at the declaration. In the example above, Goal must be initialized via a function call. This cannot occur at the declaration because the function Sense has not yet been elaborated, but it can occur later as part of the sequence of statements of the body of the enclosing package.
An unelaborated function called within a declaration (initialization) raises the exception, Program_Error, that must be handled outside of the unit containing the declarations. This is true for any exception the function raises even if it has been elaborated.
If an exception is raised by a function call in a declaration, it is not handled in that immediate scope. It is raised to the enclosing scope. This can be controlled by nesting blocks.
See also Guideline 9.2.3.
notes
Sometimes, elaboration order can be dictated with pragma Elaborate_All. Pragma Elaborate_All applied to a library unit causes the elaboration of the transitive closure of the unit and its dependents. In other words, all bodies of library units reachable from this library unit's body are elaborated, preventing an access-before-elaboration error (Rationale 1995, §10.3). Use the pragma Elaborate_Body when you want the body of a package to be elaborated immediately after its declaration.
5.9.7 Direct_IO and Sequential_IO
guideline
- Ensure that values obtained from Ada.Direct_IO and Ada.Sequential_IO are in range.
- Use the 'Valid attribute to check the validity of scalar values obtained through Ada.Direct_IO and Ada.Sequential_IO.
rationale
The exception Data_Error can be propagated by the Read procedures found in these packages if the element read cannot be interpreted as a value of the required subtype (Ada Reference Manual 1995, §A.13 [Annotated]). However, if the associated check is too complex, an implementation need not propagate Data_Error. In cases where the element read cannot be interpreted as a value of the required subtype but Data_Error is not propagated, the resulting value can be abnormal, and subsequent references to the value can lead to erroneous execution.
notes
It is sometimes difficult to force an optimizing compiler to perform the necessary checks on a value that the compiler believes is in range. Most compiler vendors allow the option of suppressing optimization, which can be helpful.
Exception Propagation
guideline
Prevent exceptions from propagating outside any user-defined Finalize or Adjust procedure by providing handlers for all predefined and user-defined exceptions at the end of each procedure.
rationale
Using Finalize or Adjust to propagate an exception results in a bounded error (Ada Reference Manual 1995, §7.6.1 [Annotated]). Either the exception will be ignored or a Program_Error exception will be raised.
Protected Objects
guideline
Do not invoke a potentially blocking operation within a protected entry, a protected procedure, or a protected function.
rationale
The Ada Reference Manual 1995, §9.5.1 [Annotated] lists the potentially blocking operations:
- Select statement
- Accept statement
- Entry-call statement
- Delay statement
- Abort statement
- Task creation or activation
- External call on a protected subprogram (or an external requeue) with the same target object as that of the protected action
- Call on a subprogram whose body contains a potentially blocking operation
Invoking any of these potentially blocking operations could lead either to a bounded error being detected or to a deadlock situation. In the case of bounded error, the exception Program_Error is raised. In addition, avoid calling routines within a protected entry, procedure, or function that could directly or indirectly invoke operating system primitives or similar operations that can cause blocking that is not visible to the Ada run-time system.
Abort Statement
guideline
- Do not use an asynchronous select statement within abort-deferred operations.
- Do not create a task that depends on a master that is included entirely within the execution of an abort-deferred operation.
rationale
An abort-deferred operation is one of the following:
- Protected entry, protected procedure, or protected function
- User-defined Initialize procedure used as the last step of a default initialization of a controlled object
- User-defined Finalize procedure used in finalization of a controlled object
- User-defined Adjust procedure used in assignment of a controlled object
The Ada Reference Manual 1995, §9.8 [Annotated] states that the practices discouraged in the guidelines result in bounded error. The exception Program_Error is raised if the implementation detects the error. If the implementation does not detect the error, the operations proceed as they would outside an abort-deferred operation. An abort statement itself may have no effect.
Summary
optional parts of the syntax
- Associate names with loops when they are nested (Booch 1986, 1987).
- Associate names with any loop that contains an exitstatement.
- Associate names with blocks when they are nested .
- Use loop names on all exit statements from nested loops.
- Include the defining program unit name at the end of a package specification and body.
- Include the defining identifier at the end of a task specification and body.
- Include the entry identifier at the end of an accept statement.
- Include the designator at the end of a subprogram body.
- Include the defining identifier at the end of a protected unit declaration.
parameter lists
- Name formal parametername formal parameters descriptively to reduce the need for comments .
- Use named parameter association in calls of infrequently used subprograms or entries with many formal parameters .
- Use named association when instantiating generics.
- Use named association for clarification when the actual parameter is any literal or expression.
- Use named association when supplying a nondefault value to an optional parameter.
- Provide default parameters to allow for occasional, special use of widely used subprograms or entries.
- Place default parameters at the end of the formal parameter list.
- Consider providing default values to new parameters added to an existing subprogram.
- Show the mode indication of all procedure and entry parameters (Nissen and Wallis 1984).
- Use the most restrictive parameter mode applicable to your application.
types
- Use existing types as building blocks by deriving new types from them.
- Use range constraints on subtypes.
- Define new types, especially derived types, to include the largest set of possible values, including boundary values.
- Constrain the ranges of derived types with subtypes, excluding boundary values.
- Use type derivation rather than type extension when there are no meaningful components to add to the type.
- Avoid anonymous array types.
- Use anonymous array types for array variables only when no suitable type exists or can be created and the array will not be referenced as a whole (e.g., used as a subprogram parameter).
- Use access parameters and access discriminants to guarantee that the parameter or discriminant is treated as a constant.
- Derive from controlled types in preference to using limited private types.
- Use limited private types in preference to private types.
- Use private types in preference to nonprivate types.
- Explicitly export needed operations rather than easing restrictions.
- Use access-to-subprogram types for indirect access to subprograms.
- Wherever possible, use abstract tagged types and dispatching rather than access-to-subprogram types to implement dynamic selection and invocation of subprograms.
data structures
- When declaring a discriminant, use as constrained a subtype as possible (i.e., subtype with as specific a range constraint as possible).
- Use a discriminated record rather than a constrained array to represent an array whose actual values are unconstrained.
- Use records to group heterogeneous but related data.
- Consider records to map to I/O device data.
- Use access types to class-wide types to implement heterogeneous polymorphic data structures.
- Use tagged types and type extension rather than variant records (in combination with enumeration types and case statements).
- Record structures should not always be flat. Factor out common parts.
- For a large record structure, group related components into smaller subrecords.
- For nested records, pick element names that read well when inner elements are referenced.
- Consider using type extension to organize large data structures.
- Differentiate between static and dynamic data. Use dynamically allocated objects with caution.
- Use dynamically allocated data structures only when it is necessary to create and destroy them dynamically or to be able to reference them by different names.
- Do not drop pointers to undeallocated objects.
- Do not leave dangling references to deallocated objects.
- Initialize all access variables and components within a record.
- Do not rely on memory deallocation.
- Deallocate explicitly.
- Use length clauses to specify total allocation size.
- Provide handlers for Storage_Error .
- Use controlled types to implement private types that manipulate dynamic data.
- Avoid unconstrained record objects unless your run-time environment reliably reclaims dynamic heap storage.
- Unless your run-time environment reliably reclaims dynamic heap storage, declare the following items only in the outermost, unnested declarative part of either a library package, a main subprogram, or a permanent task:
- Access types
- Constrained composite objects with nonstatic bounds
- Objects of an unconstrained composite type other than unconstrainedrecords
- Composite objects large enough (at compile time) for the compiler to allocate implicitly on the heap
- Unless your run-time environment reliably reclaims dynamic heap storage or you are creating permanent, dynamically allocated tasks, avoid declaring tasks in the following situations:
- Unconstrained array subtypes whose components are tasks
- Discriminated record subtypes containing a component that is an array of tasks, where the array size depends on the value of the discriminant
- Any declarative region other than the outermost, unnested declarative part of either a library package or a main subprogram
- Arrays of tasks that are not statically constrained
- Minimize the use of aliased variables.
- Use aliasing for statically created, ragged arrays (Rationale 1995, §3.7.1).
- Use aliasing to refer to part of a data structure when you want to hide the internal connections and bookkeeping information.
- Use access discriminants to create self-referential data structures, i.e., a data structure one of whose components points to the enclosing structure.
- Use modular types rather than a Boolean arrays when you create data structures that need bit-wise operations, such as and and or.
expressions
- Use 'First or 'Last instead of numeric literals to represent the first or last values of a range.
- Use 'Range or the subtype name of the range instead of 'First .. 'Last.
- Use array attributes 'First , 'Last , or 'Length instead of numeric literals for accessing arrays.
- Use the 'Range of the array instead of the name of the index subtype to express a range.
- Use 'Range instead of 'First .. 'Last to express a range.
- Use parentheses to specify the order of subexpression evaluation to clarify expressions (NASA 1987).
- Use parentheses to specify the order of evaluation for subexpressions whose correctness depends on left to right evaluation.
- Avoid names and constructs that rely on the use of negatives .
- Choose names of flags so they represent states that can be used in positive form.
- Use short-circuit forms of the logical operators to specify the order of conditions when the failure of one condition means that the other condition will raise an exception.
- Use <= and >= in relational expressions with real operands instead of =.
statements
- Minimize the depth of nested expressions (Nissen and Wallis 1984).
- Minimize the depth of nested control structures (Nissen and Wallis 1984).
- Try using simplification heuristics.
- Use slices rather than a loop to copy part of an array.
- Minimize the use of an others choice in a case statement.
- Do not use ranges of enumeration literals in case statements.
- Use case statements rather than if/elsif statements, wherever possible.
- Use type extension and dispatching rather than case statements, if possible.
- Use for loops, whenever possible.
- Use while loops when the number of iterations cannot be calculated before entering the loop but a simple continuation condition can be applied at the top of the loop.
- Use plain loops with exit statements for more complex situations.
- Avoid exit statements in while and for loops.
- Minimize the number of ways to exit a loop.
- Use exit statements to enhance the readability of loop termination code (NASA 1987).
- Use exit when ... rather than if ... then exit whenever possible (NASA 1987).
- Review exit statement placement.
- Consider specifying bounds on loops.
- Consider specifying bounds on recursion.
- Do not use goto statements.
- Minimize the number of return statements from a subprogram (NASA 1987).
- Highlight return statements with comments or white space to keep them from being lost in other code.
- Use blocks to localize the scope of declarations.
- Use blocks to perform local renaming.
- Use blocks to define local exception handlers.
- Use an aggregate instead of a sequence of assignments to assign values to all components of a record
- Use an aggregate instead of a temporary variable when building a record to pass as an actual parameter
- Use positional association only when there is a conventional ordering of the arguments.
visibility
- When you need to provide visibility to operators, use the use type clause.
- Avoid/minimize the use of the use clause (Nissen and Wallis 1984).
- Consider using a package renames clause rather than a use clause for a package.
- Consider using the use clause in the following situations:
- When standard packages are needed and no ambiguous references are introduced
- When references to enumeration literals are needed
- Localize the effect of all use clauses.
- Limit the scope of a renaming declaration to the minimum necessary scope.
- Rename a long, fully qualified name to reduce the complexity if it becomes unwieldy.
- Use renaming to provide the body of a subprogram if this subprogram merely calls the first subprogram.
- Rename declarations for visibility purposes rather than using the use clause, except for operators .
- Rename parts when your code interfaces to reusable components originally written with nondescriptive or inapplicable nomenclature.
- Use a project-wide standard list of abbreviations to rename common packages.
- Provide a use type rather than a renames clause to provide visibility to operators.
- Limit overloading to widely used subprograms that perform similar actions on arguments of different types (Nissen and Wallis 1984).
- Preserve the conventional meaning of overloaded operators (Nissen and Wallis 1984).
- Use "+" to identify adding, joining, increasing, and enhancing kinds of functions.
- Use "-" to identify subtraction, separation, decreasing, and depleting kinds of functions.
- Use operator overloading sparingly and uniformly when applied to tagged types.
- Define an appropriate equality operator for private types.
- Consider redefining the equality operator for a private type.
- When overloading the equality operator for types, maintain the properties of an algebraic equivalence relation.
using exceptions
- When it is easy and efficient to do so, avoid causing exceptions to be raised.
- Provide handlers for exceptions that cannot be avoided.
- Use exception handlers to enhance readability by separating fault handling from normal execution.
- Do not use exceptions and exception handlers as goto statements.
- Do not evaluate the value of an object (or a part of an object) that has become abnormal because of the failure of a language-defined check.
- When writing an exception handler for others, capture and return additional information about the exception through the Exception_Name, Exception_Message, or Exception_Information subprograms declared in the predefined package Ada.Exceptions.
- Use others only to catch exceptions you cannot enumerate explicitly, preferably only to flag a potential abort.
- During development, trap others, capture the exception being handled, and consider adding an explicit handler for that exception.
- Handle all exceptions, both user and predefined .
- For every exception that might be raised, provide a handler in suitable frames to protect against undesired propagation outside the abstraction .
- Do not rely on being able to identify the fault-raising, predefined, or implementation-defined exceptions.
- Use the facilities defined in Ada.Exceptions to capture as much information as possible about an exception.
- Use blocks to associate localized sections of code with their own exception handlers.
erroneous execution and bounded errors
- Use Ada.Unchecked_Conversion only with the utmost care (Ada Reference Manual 1995, §13.9 [Annotated]).
- Consider using the 'Valid attribute to check the validity of scalar data).
- Ensure that the value resulting from Ada.Unchecked_Conversion properly represents a value of the parameter's subtype.
- Isolate the use of Ada.Unchecked_Conversion in package bodies.
- Isolate the use of Ada.Unchecked_Deallocation in package bodies.
- Ensure that no dangling reference to the local object exists after exiting the scope of the local object.
- Minimize the use of the attribute Unchecked_Access, preferably isolating it to package bodies.
- Use the attribute Unchecked_Access only on data whose lifetime/scope is "library level."
- Use address clauses to map variables and entries to the hardware device or memory, not to model the FORTRAN "equivalence" feature.
- Ensure that the address specified in an attribute definition clause is valid and does not conflict with the alignment.
- If available in your Ada environment, use the package Ada.Interrupts to associate handlers with interrupts.
- Avoid using the address clause for nonimported program units.
- Do not suppress exception checks during development.
- If necessary, during operation, introduce blocks that encompass the smallest range of statements that can safe