The Psychology of Quality and More
CHAPTER 8 : Language Usage
8.11 Using functions
Function calls can be used to significantly simplify code, enabling it to be layered, hiding unnecessary process information below the current level. Parameters are always passed by value; to pass by reference requires the use of a pointer (note that it is safer to always pass by value unless the item referenced is to be changed).
8.11.1 Declaring function type
It is being explicit to always declare the type of a function, even when an int or nothing is being returned:
struct WINDOW *
‘void’ is standard in ANSI C, but not in original K&R, in which case it may be typedef'd.
8.11.2 Similar parameters in similar functions
When there is a group of functions which perform similar sorts of operation, it is being consistent to use the same type of parameter in the same order in each function. Even the same local names may be used:
ConvertHPWordToDCA( FILE *FromDoc, FILE *ToDoc );
This principle can also be used quite different functions, with general rules such as 'inputs before outputs' and 'control parameters last' being used. Note that different names should be used for different operations (although parts of names may be similar):
ClipHorizontal( int MinY, int MaxY );
8.11.3 Maintaining parameter type
When actual parameters are passed to a function, the function does not know the type of the parameters. It treats the parameter area (usually the stack) according to the formal parameters it is given. This allows all kinds of tricks to be played, some quite common, and some quite obscure:
A common usage of parameters is to interpret the address of an array element as a pointer:
This is a common trick, explicitly showing what actually happens and helping terse code to be written. On the other hand, it is being more consistent to always use arrays as arrays - this will be reflected in the function formal parameter. The actual parameter can also be made explicit by passing the address of the first element of the array, rather than just its name:
This can also be done the other way around, treating a pointer as the address of an array, although this is less common, and can result in string constants being (illegally) changed.
It is consistent and explicit to always maintain the original type of arrays and pointers in actual and formal parameters.
Parameters declared as char or short are implicitly promoted to int. Similarly float is promoted to double. This allows a fair degree of flexibility in the types that a function can safely cope with. Nevertheless, it is using the explicitness principle to use the appropriate type, casting if necessary, to ensure that actual and formal parameters are visibly of the same type.
A pointer simply points at memory, and a pointer of one type may be interpreted as a pointer of another type:
Reinterpreting pointers can be implementation-dependent and is always liable to cause problems. Where actual parameters are pointers of different type to formal parameters, they should be cast to the formal parameter type.
There is another class of usage which clearly deserves the title of 'tricky'. For example:
unsigned int Top16Bits;
Implementation-dependent tricks such as this are hazardous and should be avoided altogether.
Maintain type between actual and formal parameters
A simple rule that can be used to address the above problems is that the type of actual and formal parameters should always match, using casts where necessary (see 8.2.7).
If ANSI function prototypes are used, this will enable the compiler to be used to detect difference in types between formal and actual parameters.
8.11.4 Passing large amounts of information
Sometimes it is necessary to pass a function a significant amount of data. There are two common ways of doing this:
(a) Pass all items as multiple separate parameters
Using a large number of parameters is unwieldy and can be difficult to read and absorb.
What is a large number of parameters? A good answer is, "Where the whole function call cannot be absorbed by the reader as a single item", which suggests around seven as a reasonable maximum (see 2.9). A function call also becomes more difficult to read when its parameters wrap to the following line.
(b) Pass the items grouped in data structures
Passing data struct's which contains all or most of the items required by the called function is a powerful way of simplifying the interface, although if the structure is not cohesive (ie. the data in it is not clearly related), it can also reduce the understandability of the call.
Passing a large structure where only a few of its elements are to be accessed is hazardous, as it exposes the unused 'tramp data' to corruption (see 9.5.3).
Also, passing struct's by value is not completely portable, and is inefficient if they are large, although it does protect the data from change. It is more normal to pass pointers to struct's.
(c) Use a common data area
Using global data is seldom a good idea, and should be avoided (see 9.4).
Treating the symptoms by hiding the data in structures may not always be the best solution. It is often better to investigate the design of the function itself, which may turn out to be performing more than one function!
8.11.5 Complex parameters
It can be quite concise to include expressions in the actual parameters of a function call:
PlotPoint( MouseX + PrevMouseX * XFactor, MouseY + PrevMouseY * YFactor);
This is a form of nesting, where the reader must push the meaning of the called function onto his mind-stack whilst he works out the meaning of the parameters. The alternative is to calculate the parameters separately before calling the function:
NewX = MouseX + PrevMouseX * XFactor;
Another hazard is in the use of function calls in parameters:
PlotPoint( TrajectoryX(MouseX, PrevMouseX), TrajectoryY(MouseY, PrevMouseY);
The danger here (aside from the complexity issue) is that error returns cannot be handled. Assuming that it will never return an error is very hazardous.
8.11.6 Variable numbers of parameters
Functions such as 'printf', which use a variable number of actual parameters are easy to write in a non-portable manner, and can be messy in declaration and use. If possible, they should be avoided. Where they are necessary, care must be taken to use only defined methods, such as with varargs.h (non-ANSI) or stdarg.h (ANSI).
And the big