Next Chapter Return to Table of Contents Previous Chapter

[1-1] List of Reliability Rules

The reliability rules from the text are summarized in this list, with section-number references to the discussion of the rule.

Rule 1-1 (Section 1.1): Any macro definition containing operators needs parentheses around the entire definition. Each appearance of a macro argument in the definition also needs to be parenthesized if an embedded operator in the argument could cause a precedence problem.

Rule 1-2 (Section 1.1): Reliable modification of defined constants requires an environmental capability: there must be a means for ensuring that all files comprising a program have been compiled using the same set of headers. The UNIX make command is one such capability.)

Rule 1-3 (Section 1.1): If there are limitations on the modifiability of a defined constant, indicate the limitations with a comment:

#define EOF (-1)  /* DO NOT M0DIFY: ctype.h expects -1 value */

Rule 1-4 (Section 1.1): If one definition affects another, embody the relationship in the definition; do not give two independent definitions.

Rule 1-5 (Section 1.1): If a value is given for a #defined name, do not defeat its modifiability by assuming its value in expressions.

Rule 1-6 (Section 1.1): Use limits.h for environment-dependent values.

Rule 1-7 (Section 1.2): Use a consistent set of project-wide defined types.

Rule 1-8 (Section 1.3): Be sure that all functions are declared before use; headers are the most reliable way.

Rule 1-9 (Section 1.3): Create a project-wide "local" header for standard definitions and inclusions.

Rule 1-10 (Section 1.4): Use UPPERCASE names for unsafe macro functions, to emphasize the restrictions on their usage.

Rule 1-11 (Section 1.4): Never invoke an unsafe macro with arguments containing assignment, increment/decrement, or function call.

Rule 1-12 (Section 1.4): Whenever possible, use safe macro functions.

Rule 1-13 (Section 1.6): Use #if 0 if there is a need to comment-out sections of code.

Rule 1-14 (Section 1.6): Enclose each header in an "inclusion sandwich."

Rule 2-1 (Section 2.1): When exactness counts in converting floating-point to integer, be sure the value being converted is non-negative.

Rule 2-2 (Section 2.1): Test errno before using results from the math functions.

Rule 2-3 (Section 2.3): Use the <ctype.h> facilities for character tests and upper-lower conversions.

Rule 2-4 (Section 2.4): Make sure that Boolean variables are assigned the values zero and one. This means that the type tbool is always adequate, and if this rule is part of local standards, the types bool and tbool could be made synonymous.

Rule 2-5 (Section 2.4): Make sure that each test condition is Boolean, involving only Boolean types or relational and logical operators.

Rule 2-6 (Section 2.5): An enumeration's constants should all be initialized, or else none of them should be initialized.

Rule 2-7 (Section 2.5): Write programs as if enumeration variables could receive no values other than the associated enumeration constants. Treat the enumeration types as if they were unique types, not for any arithmetic int usages. Convert between enumeration variables and integer values only by use of an explicit cast.

Rule 2-8 (Section 2.6): Include "one-too-far" values in the ranges for variables, if they are needed for loop terminations or other testing purposes.

Rule 2-9 (Section 2.6): Function parameters accepting the size of an arbitrarily large object should be declared with size_t type.

Rule 2-10 (Section 2.7): When two unsigned int's are subtracted, convert the result using either (unsigned) or UI_TO_I.

Rule 2-11 (Section 2.7): Use IMOD (or some similar mechanism) to ensure that a non-negative modulo result is produced.

Rule 2-12 (Section 2.8): In (signed) integer arithmetic, assume that

overflow is illegal, may be detected (hence should never be programmed), and cannot be trapped or ignored.

Rule 2-13 (Section 2.9): Document the defining properties of declared names in a comment on the declaration, using a convention such as "colon followed by property."

Rule 3-1 (Section 3.1): Storage class (if any) should precede the type specifier.

Rule 3-2 (Section 3.1): If a variable has an initialization, its declaration should have a source line to itself.

Rule 3-3 (Section 3.1): Document the defining property of a data object with a comment on its declaration. Ensure that this defining property remains invariant (unchanging) as much as possible throughout the computation, and document any exceptions.

Rule 3-4 (Section 3.1): A program is easier to write correctly and to understand if all arrays are made complete before the array is used.

Rule 3-5 (Section 3.1): If an array's defining property can be true even if not all elements are defined, indicate the property on the array's declaration. For example,

char s[10];    /*: string */

Rule 3-6 (Section 3.2): Use executable assertions whenever they are simpler than the code being protected, and when the time to execute the assertions is not much greater than the time required to execute the code.

Rule 4-l (Section 4.1): In each pointer assignment, the right-hand-side value must have exactly the same ("converted") pointer type as the left-hand-side.

Rule 4-2 (Section 4.2): The default requirement for pointer parameters is that they must point to storage that is entirely defined. Whenever a pointer parameter can accept something else, this should be explicitly stated on that parameter's declaration comment.

Rule 4-3 (Section 4.3): A function in which the address of an automatic variable is assigned to a non-automatic pointer must contain a comment to that effect. In any function with such a comment, each return from the function is an event requiring verification that no dangling pointers are left.

Rule 6-1 (Section 6.1): In portable programming, do not hard-code the numeric values of structure offsets. The values may be different in each environment. Refer to members by their symbolic member names only.

Rule 6-2 (Section 6.2): Names with leading underscore should only appear in code that is privy to the internal details of the associated data structure, not in "user-level" portable code.

Rule 6-3 (Section 6.2): Use the "leading underscore" name format for tag and member names if the internal details of the structure are not to be inspected by functions outside of the package. Conversely, avoid leading underscores if the details of the structure are available for inspection by functions that use the structure.

Rule 6-4 (Section 6.3): If a structure is not well-defined when initialized to zero, document this fact in a comment. (The program will in general be simpler if the members are defined such that the zero-initialized structure is well-defined.)

Rule 6-5 (Section 6.5): In portable code, do not depend upon the allocation order of bit-fields within a word.

Rule 6-6 (Section 6.7): Regarding parameters which are pointers to structures, an "out" pointer parameter is assumed to be non-NULL, pointing to the storage for a structure of the specified type. "In" and "in-out" pointer parameters are assumed to point to a well-defined structure of the specified type. Any exceptions should be noted in a comment on the parameter declaration.

Rule 7-1 (Section 7.1): When a pointer p is passed to the free function, the programmer must determine how many pointers are pointing into the freed storage. (This number is known as the "reference count" of the storage.) Steps must be taken (such as assigning NULL) to ensure that none of these pointers are subsequently used to access the freed storage.

Rule 7-2 (Section 7.1): For every instance in which a program allocates storage, there should be an easily identifiable instance in which that storage is later freed.

Rule 8-1 (Section 8.2): Always test the returned value from fopen to be sure that the open succeeded.

[1-2] Declaring Function Parameters

The headers shown in this book show function parameters in comments associated with each function declaration, in this style:

double atof();  /* PARMS(char *s) */

In this form, the comments serve purely for documentation purposes. However, the ANSI C draft envisions a style of function declaration called a "prototype," in which the function parameters are actually stated in the declaration, like this:

double atof(char *s);

When the compiler has seen a prototype declaration, it can then perform type-checking of the arguments, much as the lint checker does. More than this, prototypes can also cause the conversion of function arguments. If, for example, the header <math.h> declares sqrt as

double sqrt(double x);

a function call such as

int i = 10;

double y;

y = sqrt(i);

will cause the conversion of the integer i to the expected double parameter type.

All of this makes it more important than ever that library functions should be properly declared, and the standard headers are the most reliable way to do this.

Some recent C compilers which are incorporating concepts from the ANSI draft have already introduced prototypes; you may be seeing them in your next compiler version. Thus, for some period of time you may be concerned to write programs in such a way that they can be compiled both by the previous generation of compilers and by the new-style compilers as well. To accomplish this, you will want to do two things:

1. During this transition, you must make sure never to depend upon the argument conversions being done by the newer compilers. Your programs must continue to follow the argument-passing restrictions of the older compilers.

2. You could, if you like, avoid declaring any argument information in your function declarations, thus keeping to the old style. You could, alternatively, package the argument declarations in a fashion which can be targeted either to old or new compilers. To do this, you could add a PARMS macro to your local header and use it in your function declarations like this:

void reverse PARMS((char *s));

The definition of PARMS for older compilers would simply discard the argument (which must be enclosed in a second set of parentheses, to allow for an arbitrary list of arguments):

#define PARMS(x) ()      /* empty parens */

The declaration of reverse as given above would produce

void reverse ();

The definition of PARMS for newer compilers would simply produce the argument:

#define PARMS(x) x

Thus the declaration of reverse would produce

void reverse (char *s);

If you adopt this transitional approach, you could take the PARMS information out of the comments, add an extra set of parentheses, and put the information onto the actual function declaration itself. I thought that it was somewhat premature to present all my examples this way in this book, but the translation from my notation should be rather easy if you want to adopt the approach described here.

[1-3] Side-Effects and Unsafe Macros

A recent compiler innovation opens the way to reliably diagnosing errors from side-effects on the invocation of unsafe macros. The White-smiths 3.0 compiler provides, as an extension, the pseudo-operator $noside(expr), which causes a compile-time error if the expr contains any side-effects. In order for the source code to remain portable, one would add a definition to a header like portdefs.h for an identifier like NOSIDE:

#define NOSIDE(expr)  $noside(expr)

Then, the definition of unsafe macros could be given in this form:

#define ABS(x)  (NOSIDE(x) < 0 ? -(x) : (x))

The compiler would then give a diagnostic if x contained side-effects.

In an environment that did not provide this extra checking, the invocation NOSIDE(x) could simply produce (x):

#define NOSIDE(x) (x)

I have not followed this style in the book, only because the release of the Whitesmiths 3.0 compiler is so recent.

[2-1] Round-off Errors in Math Functions

The output file shown as sqrtx.out was produced by the White-smiths PDP-11/23 compiler (version 2.3). Similar small round-off errors are produced by most other math libraries. Here is a similar output from the Lattice 8088 compiler (version 2.15), which uses the 8087 auxiliary FPP chip:

sqrtx86.out:

2.0    2.00000000000000044  -4.44E-16

3.0    2.99999999999999955   4.44E-16

5.0    5.00000000000000088  -8.88E-16

6.0    5.99999999999999911   8.88E-16

7.0    7.00000000000000088  -8.88E-16

8.0    8.00000000000000177  -1.78E-15

10.0   10.00000000000000176  -1.78E-15

12.0   11.99999999999999821   1.78E-15

13.0   12.99999999999999822   1.78E-15

15.0   15.00000000000000176  -1.78E-15

18.0   17.99999999999999642   3.55E-15

19.0   19.00000000000000355  -3.55E-15

20.0   20.00000000000000353  -3.55E-15

23.0   22.99999999999999642   3.55E-15

24.0   23.99999999999999642   3.55E-15

[2-2] Floating-to-Integer Truncation

As of June 1985, draft ANSI C requires that floating-to-integer truncation must be toward zero, whether the floating value is positive or negative. Since earlier C compilers may differ in the treatment of the rounding of negative floating values, the conservative course for portability is still to convert only positive values.

[2-3] Error Behavior of Math Functions

Some libraries (e.g., UNIX System V) provide more control over the treatment of errors in the math library. The user can provide a function named matherr which will be invoked if errors occur in a math function. This function could print diagnostics, terminate the execution, or specify the desired return-value.

The matherr function has not been adopted by ANSI C, so its use is not generally portable.

[2-4] Math Functions in ANSI C

These are the math functions that are declared in <math.h> in recent C libraries:

double acos(x)       - arc cosine of x

double asin(x)       - arc sine of x

double atan(x)       - arc tangent of x

double atan2(y, x)   - arc tangent of y/x

double ceil(x)       - smallest integer not less than x

double cos(x)        - cosine of x

double cosh(x)       - hyperbolic cosine of x

double exp(x)        - exponential function of x

double fabs(x)       - (floating) absolute value of x

double floor(x)      - largest integer not greater than x

double fmod(x, y)    - (floating) modulo function

double frexp(x, pi)  - normalized fraction

double ldexp(x, n)   - computes x times 2**n

double log(x)        - natural log of x

double logl0(x)      - base-10 log of x

double modf(x, pd)   - compute integer and fractional part of x

double pow(x, y)     - raise x to the power y

double sin(x)        - sine of x

double sinh(x)       - hyperbolic tangent of x

double sqrt(x)       - square root of x

double tan(x)        - tangent of x

double tanh(x)       - hyperbolic tangent of x

In this tabulation, these are the types of the parameters:

double x, y;

double *pd;

int n;

int *pi;

[2-5] Character-Test Functions in ANSI C

These are the math functions that are declared in <ctype.h> in recent C libraries:

int isalnum(c)  - is c a letter or a digit?

int isalpha(c)  - is c a letter?

int iscntrl(c)  - is c a non-printing character (other than space)?

int isdigit(c)  - is c a digit?

int isgraph(c)  - is c a printing character (other than space)?

int islower(c)  - is c a lower-case letter?

int isprint(c)  - is c a printing character (including space)?

int ispunct(c)  - is c a punctuation character?

int isspace(c)  - is c a whitespace character?

int isupper(c)  - is c an upper-case letter?

int isxdigit(c)  - is c a hexadecimal digit?

int tolower(c)  - convert upper-case c to lower case

int toupper(c)  - convert lower-case c to upper case

[4-1] Question

Just to be sure that you understand the "sandwich rule," jot down the variable name and type for the following declarations.

DECLARATION     VARIABLE NAME   TYPE ("DECLARED")

short **ap[5];       ap            short ** [5]

long (*pf)[2];       pf            long (*)[2]

double **pps;        pps           double **

Go to Bibliography Return to Table of Contents