The Psychology of Quality and More
CHAPTER 8 : Language Usage
8.2 Using expressions
Expressions are one of the basic building blocks of C, being used in many constructs. Complex expressions are very easy to generate, but can also be very difficult to interpret and are prone to obscure problems.
8.2.1 Pointer expressions
Pointers are one of the most powerful, and consequently one of the most dangerous, elements of the C language. It is very easy to create dense pointer expressions which are difficult to understand and maintain:
The complexity of this is aggravated by the number of structure levels, the auto-incrementing and the different addressing modes (structure member, structure member pointer and array). Using the principle of breaking down complexity (see 8.1.1), this can be turned into several smaller expressions, for example using one line per structure:
pCurrLine = &pCurrPara->Line[LineNo++];
This requires the definition of several intermediate variables, and care must still be taken over addressing mode. In complex situations like this, it is sometimes more appropriate to redesign the data structures than to redesign the code.
8.2.2 Conditional expressions
Conditional expressions, typically embedded in other expressions, are a classic case of simplicity leading to difficulty of understanding (see 3.3). Consider a simple example:
Area = Width * ( (IsAboveGround) ? Height : Depth );
This type of expression is very easy to write, and it is credible that it is written with good programming style. After all, it makes careful use of the available constructs of the language, without excessive complication. The final test must be, as always, the ease with which the reader can understand the code. In which is it easier to understand the meaning of the code, the example above, or the example below?:
if ( IsAboveGround )
The second example takes four lines and repeats several parts. It may even take more memory and take (marginally) longer to execute. It nevertheless its purpose immediately clearer, especially to a less cogent programmer than you.
Overall, conditional expressions should be used with very great care, if at all. Their use will often merit careful commenting.
8.2.3 Bit-level expressions
Expressions which involve manipulation of bits are seldom immediately clear, and arealso liable to be non-portable. Using bit expressions for 'performance' tweaks is particularly poor style:
(WordLen << 3) + (WordLen << 1); /* = WordLen * 10 */
Bit manipulation should only be used when it really is necessary, for example when interfacing to hardware registers. Also, unless there is a specific reason otherwise, bit variables should be unsigned, to avoid the vagaries of the sign bit.
Putting assignments in expressions can save space, but at the cost of complexity:
WindowArea = (WinWidth = Window.Width * PIXEL_WIDTH)
This type of expression becomes complex very quickly. Breaking it down into its constituent parts often makes it more readable:
WinWidth = Window.Width * PIXEL_WIDTH;
Assignments in comparisons
A common use of assignment in an expression is in a comparison, to check the return status value from a function:
while ( (ch = getchar()) != EOF )
This could be replaced by:
ch = getchar();
This, however, is significantly more complex than the original method. A ‘continue’ in the loop would also not work as expected. For this reason (and because of common usage), this construct is worth retaining.
A comparison is simply an equality-expression which is 'true' when it is non-zero. Where used, it should be clear what is being compared with what, particularly in complex comparisons. This may seem obvious, but it is very easy to write C comparisons which are far from clear.
Comparison with zero
The implicit comparison with zero means that a comparative expression can ignore the '!= 0', even though it may read strangely:
if ( strlen(EmpName) )
Logically speaking, a comparison should evaluate to a boolean value, true or false. Thus it is using the principle of explicitness to require that all comparisons should visibly result in a boolean value:
if ( strlen(EmpName) != 0 )
The test against non-zero can be left out only if the comparison is clearly evaluating a boolean expression:
if ( DoorIsOpen )
Note that this applies equally to pointers, which should be compared with NULL (see 9.8.2).
Comparisons in expressions
As a comparison evaluates to either 1 or 0, it is possible to use this in expressions:
FebDays = (IsLeapYear == TRUE) + 28;
This is a conditional expression in a more obscure form and thus is likely to confuse a non-guru reader. Its only redeeming feature is that it can save several lines of code.
Assignment and equality
There is often confusion and error around assignment '=' and equality '==' operators, which can lead to situations such as:
if ( DayLength = WORKING_DAY ) /* legal, but probably wrong */
This is legal but deliberate use is confusing and the reader is likely to assume it is an error. A workaround is to put the constant first, which will cause a syntax error as a constant may not have another value assigned to it:
if ( WORKING_DAY = DayLength ) /* will cause syntax error */
This, however, does not read as naturally as the first method, and may be preferable to use a language checker (see 3.15) to check for such problems.
The comma operator can be used to separate statements in a similar manner to semicolons:
WindowNo++, OpenWindow( WindowNo );
This is contrary to the principle of 'one action per line', and may lead to confusion, for example when using comma expressions in function calls or array subscripts:
/* this has two actual parameters */
Comma expressions have sufficient potential for hazard that they should generally be avoided except in limited, controlled situations such as for loop initializations.
Casting is used where the type of an expression must be changed to match the situation. Some casting is implicit, and some must be explicitly written. It saves obscure errors and warns the reader of type changes if implicit type changes are made explicit.
When expressions include variables of (or sub-expressions that evaluate to) different types, it is being explicit to cast all items to the longest type. This warning is particularly useful where truncation will take place. Casting situations include comparisons, where like type should always be compared with like type, and assignments, where the entire right hand side may be cast:
unsigned int Days;
Changing pointer type must be done carefully, even when using casts (see 9.8.4).
Similarly, function arguments of the wrong type should be cast to the expected type, especially where function prototypes (which cause automatic coercion) are not used. This is also true of the null pointer which may otherwise get passed as an integer!:
CalcRoomVolume( pRoom, (MODEL *)NULL );
Items return'ed from functions should be cast to the function type. If a function return is not being used, then this should be explicitly shown by casting the function to void:
8.2.8 Autoincrement and autodecrement
These operators are commonly used to simplify code. The only problem is that although (or perhaps because) they are concise in space and action, they can contribute to a dense expression which is less easy to decode. There is also the hazard of side effects, such as using a variable which is auto-incremented or -decremented in the same expression:
while ( (CurrName[i++] == SearchName[j++])
Autoincrement and autodecrement should not be over-used or abused, and care should be wherever they are used to ensure correct and clear usage.
When there is a choice...
When it does not matter whether you use pre- or post-increment and decrement, which do you use? This is another of those little things about which it is worth being consistent. Alternatives and arguments are:
And the big