Routine documentation should describe what a routine does, not how it does it. Start with a one-sentence summary. For each parameter, describe what it is used for. Do the same for all global variables a routine uses: they are invisible parameters. State what assumptions a routine makes, and what it does if it detects an error.
Variables should be accompanied by a comment describing their purpose, if that is not obvious from the variable's name. A single-line comment is usually sufficient.
A group of statements that perform a non-trivial task should be preceded by a comment summarizing that task. Do not reiterate in English what is obvious from the code (e.g., "variable x is incremented").
Have a clean interface to the module. Do not pass unnecessary information.
Typically a program spends 90% of its execution executing 10% of its code. Further, it is not always obvious what code constitutes that 10%. If efficiency is of paramount importance, find the 10% and do what it takes to make it efficient. Writing obscure "efficient" code involving side-effects is a waste of time and makes maintenance difficult if it is not in the 10% region. Remember, the point of an assignment is not ultra-efficiency, it is conveying an understanding of the material through a clear, concise, and well-developed program.
However, you are not required to follow this standard. In fact, you are encouraged to mix cases, for example:
SizeOfArray
MaxElements
/* /*
* comment ... or comment ...
*/ */
These forms allow new lines to be added without changing existing lines in a program.
Single-line comments may also be written as follows:
statement; /* simple descriptive comment */
Try to align all the comments at a particular column, for example:
statement; /* simple descriptive comment */
statement; /* simple descriptive comment */
statement; /* simple descriptive comment */
which is easily done using tabs to move to a particular fixed column.
However, there is one situation where global variables can be used, that is, in simulating a module or package. Variables declared in the external scope are implicitly local to that text file. These variables correspond to the private variables of a module or package. Routines that are exported (non-static) can use these variables without requiring users to explicitly create them and pass them as parameters. Declarations in the external scope must always state their purpose, the rules for using them, and which routines use them. In C++, classes can generally be used to solve such problems without the use of global variables.
#define PI 3.14159
#define PI_4 (3.14159 / 4.0)
a global declaration with a const specifier should be used instead, for example:
const float pi = 3.14159;
const float pi_4 = 3.14159 / 4.0;
Note that a macro definition that is more complex than a constant should always be surrounded by parentheses to avoid side effects when expanded. Furthermore, if the macro has parameters, the parameters should not appear multiple times in the macro definition as this can result in multiple side effects when the argument is evaluated multiple times. Macros are not routines!
A = B * (C+ D % 3 ); vs A=B*(C+D%3);
if ( a == b )
{
a = 3;
b = 5;
}
else
{
|
if ( a == b ) {
a = 3;
b = 5;
} else {
a = 2;
b = 7;
} /* if */
|
This extra visibility is critical during all aspects of program development. Until such time as we all have access to terminal screens that can display as much information as is printed on a page of a listing, it is recommended (but not required) that you forego extra vertical whitespace in favour of increased information display.
All closing braces must start on a separate line and, in general, it is a good habit to label them as to the type of control structure or routine they terminate. Labelling exceptions are "} else {" and "} while ( expr )" which are self documenting. Documenting closing braces allows identification of a terminating block when the start of the block is not visible. This can occur because of page boundaries in a listing or screen boundaries on a terminal. For example, if you need to insert a line after the end of the second if statement in the following nested control structures and this is all that is visible on your screen:
}
}
}
}
|
} // if
} // while
} // if
} // for
|
it is much easier (and takes less time) to find the desired location if the closing braces are labeled appropriately.
{
int x = 3; // first binding is not a side-effect
... // during this part of the block, x is bound to 3
x = 4; // an operation that causes a side-effect
... // during this part of the block, x is bound to 4
}
Functional programming [Rea89], represented by languages like FP and pure LISP, has no side effects, which means there is only initialization at declaration;
imperative programming, represented by languages like Ada and C/C++, has side effects after a declaration.
While imperative programming uses side effects, reducing the number of side effects and/or the locations where they occur can decrease the complexity and increase the maintainability of a program.
(In a similar compromise, functional programming occasionally uses assignment to increase performance.)
Therefore, it is good programming practice to reduce the number of side effects and define precisely the locations where side effects occur.
In this course, these locations are limited to the last operation of an expression through the assignment operator. In general terms, there can be a maximum of one side effect on the left margin of a line of C/C++ code. This rule results in the following restrictions in C/C++:
Unfortunately, C/C++ programmers have a tradition of using multiple side effects, particularly in expressions, usually to gain efficient execution because of poor compilers or deficiencies in C. Fortunately, compiler technology has improved to the point where it is no longer necessary to write such code. The following is a typical C/C++ expression with multiple side effects in an expression and its corresponding multi-line version:
int getc1(FILE *p) {
return (--(p)->_cnt>=0 ? ((int)*(p)->_ptr++) : _filbuf(p));
} /* getc1 */
int getc2(FILE *p) {
int r;
p->_cnt -= 1;
if (p->_cnt >= 0) {
r = (int)*(p)->_ptr;
p->_ptr += 1;
} else {
r = _filbuf(p);
} /* if */
return r;
} /* getc2 */
While both routines may be considered to be complex, the latter is clearly easy for most C/C++ programmers to understand and modify.
Further, the compiler used in this course generates almost identical code in both cases (1 instruction difference).
Certain deficiencies of C/C++ may require multiple side effects in expressions, but they will not appear in this course.
The following discussion is further motivation for these restrictions.
i++
...
for ( i = 0; i < 10; i++ ) ...
The preferred style is to use operators like += and -=, since they provide the same capability and are more general, as in:
i += 1;
...
for ( i = 0; i < 10; i += 1 ) ...
a[i * j / 2] *= k[l * m + 3]; vs a[i * j / 2] = a[i * j / 2] * k[l * m + 3];
Not duplicating the left-hand side of the expression on the right-hand side reduces error.
Further,
i += 1 changed to i += 3;
i++ changed to i++ ++ ++; // this is illegal for other reasons
Contrary to what some C books suggest, there is no performance advantage in either the ++ or -- operator with modern C/C++ compilers and machines. For example, most of todays C/C++ compilers generate exactly the same code for:
i++;
i += 1;
i = i + 1;
Finally, both beginners and experienced C/C++ programmers often make mistakes with operators ++ and --. An experienced programmer on the net wrote:
A function had an output parameter which was a numeric counter, i.e., a pointer to an integer. I wrote the code to increment the counter as *count++, which does the wrong thing (it should be (*count)++), or, as I rewrote it, *count += 1.
[ x, y ] = f( a, b ); /* not allowed in C/C++ */
Instead, this can be done by returning values through the argument-parameter mechanism. Therefore, the call must be rewritten as:
x = f( &y, a, b ); or f( &x, &y, a, b );
where routine f has one or two output parameters, respectively.
Alternatively, a routine can return a structure, rather than a simple variable, thus allowing each component of the structure to be a return value. However, it is probably not a good idea to define a structure only to serve as the return value of a particular function.
while ( cin.get(c) != '\n' ) . . .
which could become:
cin.get(c); // duplicate code
while ( c != '\n' ) {
...
cin.get(c); // duplicate code
} /* while */
Fortunately, the duplicated code in this case can be eliminated by using a multi-exit loop, as in
for ( ;; ) {
cin.get(c);
if ( c == '\n' ) break;
...
} /* for */
You are strongly encouraged to use multi-exit loops to eliminate duplicate code [Buh85].
Further, the compiler for this course generates code of identical efficiency for the multi-exit loop and the first while loop with the side effect in its expression.
However, all exit points should be highlighted by outdenting or with comments to make them easy to locate.
(This document uses outdenting.)
| Poor Logic | Better Logic |
|---|---|
int r( int x, int y ) {
if ( x > 3 ) {
x += 1;
if ( x > y ) {
x = y;
} else {
return x + 3;
}
return x + y;
} else if ( x < 3 ) {
return 3;
} else {
return x;
}
}
|
int r( int x, int y ) {
if ( x < 3 ) return 3;
if ( x == 3 ) return x;
x += 1;
if ( x > y ) {
x = y + y;
} else {
x += 3;
}
return x;
}
|
As with multi-exit loops, return points should be highlighted by outdenting or comments to make them easy to locate.
/*********** SearchInsert ***********
Purpose: An element is looked up in an unsorted list of items,
if it does not appear in the list, it is added to the end of the list,
if it exists in the list, its associated list counter is incremented.
Returns: Position in list of key.
Errors: List is full and cannot insert item. Program is terminated.
Globals:
MaxListSize - used for error checking
Elem - type of a list element
************************************/
int SearchInsert(
Elem list[ ], // array of items to be searched and possibly modified
int &listSize, // reference to number of items in list, MAY BE MODIFIED!
int key // item that is searched for in the list
) {
int pos;
// loop has two exits: search does not find key and key is found
for ( pos = 0; ; pos += 1 ) {
if ( pos >= listSize ) { // if key not found, insert
listSize += 1;
if ( listSize > MaxListSize ) { // list full ?
cerr << "ERROR: List is full." << endl;
exit(-1); // TERMINATE PROGRAM
} // if
list[pos].count = 1;
list[pos].data = key;
break;
} // exit
if ( key == list[pos].data ) { // if key found, increment counter
list[pos].count += 1;
break;
} // exit
} // for
return pos; // return position of key in list
} // SearchInsert
Figure 1: Sample C/C++ Programming Style