Aussie AI

C++ Expression Bugs

  • Bonus Material for "Generative AI in C++"
  • by David Spuler, Ph.D.

Expression Bugs

Expressions in C++ programs can become very complicated and are prone to error. The sheer number of different operators can cause confusion, and errors occur when operators are used improperly or are applied in the wrong order. Some of the common types of errors in arithmetic expressions include:

  • Using "=" assignment not "==" for equality test
  • Divide by zero (integer or float) will crash
  • Modulus (%) by zero (integer) will crash
  • Integer division truncates fractions
  • Modulus (%) by negative is undefined
  • Right bitshift (>>) on a negative is undefined

Some more complicated errors in arithmetic expressions include:

  • Precedence of * and + (just like in High School)
  • Precedence of && and || (logical operators) in conditions
  • Precedence of bitwise operators
  • Precedence of assignment operators
  • Short-circuiting of && and ||
  • Order of evaluation of operands with side-effects

Special cases and boundary conditions

Algorithms often fail on special cases. It is a good idea to ensure that your algorithms correctly handle all special cases. Test the program on all special cases, such as those involving zero or one of some object, or the maximum number of such objects. Some examples of special cases (often called boundary conditions) are:

  • empty arrays (n=0)
  • full arrays (n=MAX)
  • empty lists
  • single element lists

Equality is often a special case. Always be careful when using the relational operators, greater-than and less-than. Does the algorithm make the correct choice on equality? Mistakes are commonly made with < instead of <=, and vice versa.

Off-by-one errors

Off-by-one errors refer to errors where the program does one too many or one too few actions. They are often referred to as "fencepost" errors.

A common example of off-by-one involves problems with array indices. Remember that array indices range from 0..n-1, and not 1..n. When counting elements in an array, there are n elements, but the highest index is n-1. If the size is confused with the highest index, the program is off-by-one.

Another example of off-by-one is whether to increment before or after an operation (i.e., prefix or postfix increment). Consider the for loop to copy a linked list into an array:

    int count = 0;
    for (p = head; p != NULL; p = p->next)
        a[++count] = p->data; /* INCORRECT */

This is incorrect as the zeroth array element is missed. The problem is the use of the prefix ++ causing count to be incremented before its value is used for the array index. The correct statement uses post-increment:

    a[count++] = p->data; /* CORRECT */

Another example of an off-by-one error occurs in the use of arrays. Common style for setting or accessing all elements in an array is to use a for loop similar to:

    for (int i = 0; i < n; i++)
        a[i] = 0;
There are two ways this can go wrong — i can be wrongly initialized to 1, or the < operator can be mistakenly replaced with <=. In the first case, the zeroth array element is missed. In the second case, the array element a[n] is accessed, often causing a crash.

Assignment vs Equality (= vs ==)

The most common error for beginning C++ programmers is to use the assignment operator (=) instead of the relational equality operator (==). The assignment operator is legal in if statements, which is unfortunate when learning the language, although useful for advanced C++ programmers.

The assignment operator (=) evaluates to the value of its right operand. In the example below, the value 3 is assigned to x, and the result returned is 3, which is always true.

    if (x = 3)   // Incorrect

If instead there had been "if(x=0)" it would always be false, because the value of the expression "x=0" is 0, which is false. The problem is illustrated below:

    if (x == 3) // Correct

There is no easy way to avoid this error. Many compilers do not consider assignment in logical conditions to be an error and do not generate any warning. One common way to reduce the occurrence of these errors is to put the constant first:

    if (3 == x) // Correct

In this reversed syntax, if you accidentally type only "=" then "3=x" is not a valid assignment, and you get a weird compiler error about an "l-value". However, the only real solution is practice — get used to typing ==.

Confusing logical and bitwise operators (& and &&)

Another common mistake that novice programmers make with double character operators is using & or | (which are bitwise operators) instead of && or || (which are logical operators). This bug is not detected by the compiler, as it is not a syntax error.

The erroneous use of bitwise operators can cause intermittent bugs. The difference between & and && does not always cause an error. The bitwise-and operator, &, causes a bit operation on each bit of its two operands, and returns the result, which can be any integer. The logical operator, &&, returns only two possible values — zero or one. It returns 1 (true) if both operands are non-zero, otherwise it returns zero.

If both operands are either zero or one, there is no difference between the result of & and &&. This is often the case in conditions such as "(x==y)&&(z>3)", because the relational operators return only zero or one. However, problems occur when using a non-zero value to indicate the truth of a condition. Incorrect results can occur below if & accidentally replaces &&.

   if (flag && x > y)    // if flag != 0 and x > y

If & replaces && here, the test is effectively a bit mask on the lower bit of flag. The test "(x>y)" returns 0 or 1 which is then used as a mask for the & operator. If, for example, flag is 2 (non-zero means true) the condition will return false anyway, because the lower bit is zero.

There are also differences between & and && in terms of their order of evaluation. The logical operator && uses short-circuiting and does not evaluate its second operand if the first operand is false. In the code below the incorrect use of & instead of && will cause an error if p is NULL:

    if (p == NULL & p->next != NULL)  // Bug

The preprocessor could be used to help:

  #define and &&
  #define or  ||
  #define bitand &
  #define bitor  |
  #define bitxor ^
This is probably not a bad idea, but is not widely recommended, mainly because it is not common practice. The best solution to this problem is to be aware of it, and get used to typing characters twice in C++ expressions. It is also good to use explicit comparisons with zero (i.e., flag!=0) since this reduces dangers of error, increases readability, and should not reduce efficiency on any reasonable compiler.

Boolean expressions: De Morgan's laws

A common mistake for novice programmers is to incorrectly interpret complicated Boolean expressions. Unless you are very familiar with the rules of Boolean algebra, check these expressions carefully. A good idea is to hand-evaluate a few cases to check that the condition gives the correct result.

One problem area involves the combination of ! with either of && or ||. De Morgan's laws of Boolean algebra are counter-intuitive. For example, do not be fooled into thinking that "not (A or B)" is the same as "not A or not B". De Morgan's laws state:

    !(x || y) == (!x) && (!y)
    !(x && y) == (!x) || (!y)

Incorrect Range Comparisons

When testing whether x is in the range 1..10, the expression is simply:

    if (1 <= x && x <= 10)    // Correct

Problems may arise when this if statement is inverted to test whether x is not in this range. An obvious (and correct) method is to use the ! operator:

    if (!(1 <= x && x <= 10))  // Correct

However, many programmers are tempted to simplify this code for efficiency (although in practice a good compiler will perform these changes automatically during code generation). The trap here is to change just the <= operators without changing the && operator to ||:

    if (1 > x && x > 10))   // Bug
The above test is testing whether x is less than 1 and greater than 10 — obviously an impossible condition. The correct solution is to change both the relational operators (<=) and the logical operator (&&):
    if (1 > x || x > 10))   // Correct

No Exponential Operator

There's no C++ operator for powers or exponentiation. Try this:

    int x = 2 ^ 3;  // Not 8!
    cout << "X = " << x << "\n";

The "^" operator is bitwise-exclusive-or (XOR) rather than a power or exponentiation operator. The correct code is to use the C++ pow or "exp" functions. Or if you're trying to do integer powers of 2, you can use bitshifting instead:

    int x = 1 << 3;  // 2^3=8 with "<<" bitshift
    cout << "X = " << x << "\n";  // Outputs X=8

Float equality tests with zero

Real numbers can behave strangely on computers. They are rarely exactly as they seem. Results of computations always differ at the tenth or fifteenth decimal place. Guaranteed.

The problems are due to the internal representation of real numbers. Computers can only store real numbers to a limited precision (i.e. a limited number of significant figures). Inevitably, roundoff errors occur in calculations

The most common mistake made with real numbers is comparing them for equality. For example, the for loop below checks for equality (i.e. not equal):

    float inc = 0.1f;
    for (float x = 0.0f; x != 10 * inc; x += inc)  // Might fail

This could cause an infinite loop, because when x nears 10*inc, it might not be exactly equal to ten or so decimal places. The test for equality would fail because of a tiny difference, and the loop would continue infinitely, incrementing x further away from 10*inc at each iteration.

Instead of comparing two real numbers for exact equality, programs should examine their difference. When the absolute value of this difference is smaller than some defined tolerance, consider them equal. A greater difference than the tolerance means not equal. The tolerance value should be very small (e.g. 0.00001). The for loop becomes:

    float inc = 0.1f;
    const float TOLERANCE = 0.001f;
    for (float x = 0.0f; fabsf(x - 10 * inc) > TOLERANCE; x += inc)

Note that there are suffix "f" letters everywhere. The constant "0.1f" is "0.1" but of type float (not double). Note that "fabs" is the floating-point absolute value function, and "fabsf" is the version of this math function which accepts a float as argument, and returns a float.

Note that changing the original loop to use < instead of != would also probably fail, although not as drastically. Consider the loop with a "<" test:

    for (float x = 0.0f; x < 10 * inc; x += inc)  // Might fail

This loop might not iterate exactly 10 times. It might perform one extra iteration because at the end of the 10th iteration, the value of x might be very close to 10.0*inc, but not exactly equal, causing the < operator to still return true (it is still less than, albeit only very slightly).

Tests for real number equality might be better hidden behind a macro or inline function:

    #define REAL_EQ(x,y) (fabs((x) - (y)) <= TOLERANCE)
    #define REAL_NEQ(x,y) (fabs((x) - (y)) > TOLERANCE)
    inline bool REAL_EQ(double x,double y) {return fabs(x-y) <= TOLERANCE;}
    inline bool REAL_NEQ(double x,double y) {return fabs(x-y) > TOLERANCE;}

This idea should also have "float" versions that call the "fabsf" function.

Operator precedence errors

The precedence of some C++ operators is not as you might expect it to be and nasty bugs can creep in. The only real solution is to become more familiar with the precedence of various operators, but placing extra brackets causes no harm and also does eliminate the problem.

High School math mistake: Don't forget that "*" and "/" have a higher precedence than "+" or "-". Here's the tricky formula for the sum of the numbers from 1 to n, as you learned in algebra:

    sum = n * n - 1 / 2;   // Bug

This expression incorrectly does "n*n" first, and then "1/2", and then subtracts them, which isn't what you wanted. The correct code uses parentheses to get the correct ordering of operators:

    sum = (n * (n - 1)) / 2;   // Correct

Note that the modulo (%) operator for the remainder also has a higher precedence.

Bitwise operator precedence errors: When masking bits and then comparing them with a value, brackets are needed to ensure the correct ordering:

    if (x & MASK != 0)     // Bug

The fix is to use parentheses:

    if ((x & MASK) != 0)   // Correct

Assignment operator precedence: Similarly, when testing a function return value by assigning it to a variable (i.e. a legitimate use of the "=" assignment operator inside an if statement), brackets are necessary:

    if (ch = getchar() != EOF)     // Bug
    if ((ch = getchar()) != EOF)   // Correct

Bitshift operator precedence errors: When using the shift operators to replace multiplication or division, the low precedence of the bitshift operators causes problems:

    x = a + b >> 1;    // Bug
    x = a + (b >> 1);  // Correct

Pointer increment/decrement precedence: The use of increment or decrement operators with either of the pointer dereference operators is quite dangerous. There are always two interpretations, depending on whether brackets are used.

    ++p->len;     // Increment p->len
    (++p)->len;   // Increment p, then access p->len
    *p++;         // Increment p, then access what p points to
    (*p)++;       // Increment what p points to

The last two statements illustrate a common error: the use of *p++ is incorrect when trying to increment what p is pointing to.

Type cast operator precedence error: The high precedence of the type cast operator means that care is necessary when using it in expressions. For example, when trying to convert the result of x/y to int, the code below is incorrect:

    z = (int) x / y;       // Bug
The correct method is to use brackets:
    z = (int)( x / y );    // Correct

Iostream precedence: The use of the bitshift operators with C++ output streams declared by iostream can occasionally be the cause of a precedence error. There is no problem for any operators with higher precedence than "<<", and this includes the most used operators. However, the relational operators, bitwise operators, logical operators, and the conditional operator can all cause precedence errors because of their low precedence. The code fragment below illustrates these dangers:

    cout << x & y;     // Compilation error
    cout << x > y;     // Compilation error
    cout << x && y;    // Warning?
    cout << x ? x : y; // Warning?

The use of the bitwise or relational operators will cause a compilation error because there is an attempt to apply an operator such as & or > to a stream object. However, the incorrect use of the && operator will receive only a warning about the value of the && expression not being used, and not all compilers will have warnings this sophisticated. Similarly, the use of the conditional operator may not even receive a warning. The solution is simply to bracket the subexpressions being printed:

    cout << (x & y);    // Correct
    cout << (x > y);
    cout << (x && y);
    cout << (x ? x : y);

Null effect operations

This error refers to use of operators where the returned result is ignored. It can occur due to a major misconception about some operators, or because of operator precedence problems.

Null-effect bitwise operators: For example, the statements below have no effect because they are using the wrong operators:

    x << 1; // Incorrect
    ~x;     // Incorrect

The first statement is an attempt to double x using bit shifting, and the second is an attempt to complement x using the one's complement operator. Unfortunately, the << operator in the statement "x<<1;" does not affect x at all. Instead, the statement merely evaluates the value of x doubled and then "throws away" this value because it is not used.

You might get a "null effect" warning from the compiler, but it won't be an error. Compilers usually do not complain about throwing away values because it is commonly used in function calls (e.g. the printf function always returns a value, but this value is rarely used) and the statements above are executed normally (although they actually do nothing at all).

The correct statements are:

    x <<= 1;  // Correct
    x = ~x;   // Correct

Null effect pointer dereference: Another example of the problem, related to operator precedence, occurs in the statement:

    *ptr++;   // Incorrect

The intention is to increment what ptr is pointing to, but operator precedence causes ++ to be evaluated first, followed by the "*" dereference operator . Hence, ptr is incremented, and the dereference operation (*) has no effect (the value of what ptr points to is calculated and then thrown away). Brackets are needed to enforce the correct precedence:

    (*ptr)++;   // Correct

Function Call Without Parentheses

Another common example of null effect statements occurs when novice programmers call a function with no arguments and omit the empty pair of brackets, as below:

    exit;   // Incorrect

This is interpreted as a null effect statement that simply calculates the address of the function, rather than calling the function. The corrected statement is:

    exit();   // Correct

Missing Member Function Parentheses: The situation is also dangerous in C++ where omitting the brackets from a call to a member function with no arguments can cause wrong behavior. An example is:

    stack.pop;   // Bug: should be stack.pop();

Even worse is the fact that most compilers will not detect missing brackets on function calls in conditional expressions. For example, consider the C++ expression with missing brackets:

    if (stack.is_empty) { // Bug
        ....
    }

This may not provoke a warning on many C++ compilers. It is interpreted as testing whether the pointer-to-function constant "stack.is_empty" is not equal to nullptr; therefore the condition will always evaluate to true. Therefore, hopefully it will get a compilation warning.

Side effects missed by short-circuiting

In expressions that use the binary logical operators (|| or &&) or the ternary operator (?:) novice programmers can have problems if the sub-expressions contain side effects. Experienced programmers know about the short-circuiting, and use it to advantage.

If the first operand of an && is false, or the first operand of an || is true, then the second operand is not evaluated at all. If either of these cases occurs, the result is completely determined by the first operand. There is no need to evaluate the second operand to determine the result. The identities below show the justification for short-circuiting:

    False AND anything == False
    True OR anything == True

Short-circuiting improves the efficiency of evaluating a Boolean expression by doing as little work as necessary. Howev er, it can also cause strange errors.

Side effects are operations that either affect a variable, consume input or produce output. Thus, the increment (++) and decrement (--) operators and the assignment operators (i.e. =, +=, etc.) all cause side effects because they change a variable. A function call can also be a side effect if it consumes input, produces output, changes one of its arguments (e.g. memcpy) or alters a global variable.

The problem is that C++ uses short-circuiting in the evaluation of the binary logical operators and the ternary operator. This means that not all sub-expressions in an expression with these operators are always executed.

If a sub-expression containing side effects is not executed, the (usually important) effect of these side effects is lost. For example, consider the expression:

    if (x < y && printf(...))

If the first term (x < y) is false, the call to the printf function is not executed and no output is produced. The only real solution is to avoid the use of side effects in Boolean expressions. This is quite reasonable since such expressions are bad style anyway.

Side effects to sizeof are ignored

The sizeof operator can be applied to an expression, and yields the size of the type of the resulting expression. Any side effects in this expression are not evaluated at run-time. Hence, it is reasonably common practice to call malloc as below:

    ptr = malloc(sizeof(*ptr));   // Correct

The expression "*ptr" is not evaluated, so there is not the delightful side-effect of a crash if ptr is a null pointer. The above code is safe. However, consider if the programmer wants side-effects to occur with a more tricky version:

    ptr = malloc(sizeof(*ptr++));   // Bug

The tricky version tricks the programmer instead. The side-effect "ptr++" never occurs, and the pointer is unchanged. sizeof is a compile-time operator that does not execute run-time side effects, and there is often no warning from the compiler.

sizeof array parameter

There is another situation when the sizeof operator computes surprising results — when applied to a function parameter of array type. The error is illustrated by the following function:

    void test_sizeof(int arr[3])
    {
        printf("Size is %d\n", (int) sizeof(arr));
    }

The computed size is expected to be 3*sizeof(int) — usually 12. However, the actual result will usually be 4 or 8. This is because the sizeof operator is actually being applied to a pointer type. An array parameter is converted to the corresponding pointer type and it is this type that sizeof applied to. Therefore, the output result is exactly sizeof(int*), which is the size of a pointer, commonly 4 or 8.

sizeof and string literals

Another bizarre situation related to sizeof in C++ is that the computed size for a string literal is not the same as a pointer size. For example, sizeof("hello") is not 4 or 8 (i.e., a pointer size), but is actually 6, because "hello" has array type char[6], with 5 bytes for the letters and one byte for the null.

Overflow of 16-bit short ints

If you are working with 16-bit short types, such as to workaround issues related to FP16 representations, be aware that they can easily overflow or underflow. The range of a 16-bit int is −32,768...32,767 which is quite small.

Numbers like 50,000 and 100,000 are commonly used in programs and the programmer must take that they are processed by int rather than short. If a value stored in a short becomes too large (or too small) it will overflow and change sign, leading to strange errors.

Overflow of 32-bit int

By contrast, a 32-bit int will have a huge range of values (−2,147,483,648...2,147,483,647) and is quite unlikely to overflow in common usage. However, when performing mathematical computations involving integers, the choice between int and long should be made carefully. For example, consider the factorial function to compute the product 1*2*3...*n. The use of int as the return type is shown below:

    int yapi_factorial(int n)
    {
        int result = 1;
        for (int i = 1; i <= n; i++)
                result *= i;
        return result;
    }

This function looks innocuous, but it will overflow a 32-bit int on an input as low as n=14. It needs to be changed to "long int" type, and even then, it needs an input check added for n being too large.

Array index out of bounds

During execution, a C++ program does not check array references to determine if indices are too large for the array. A very common error is an array index passing the end of the array. This can cause other variables to be overwritten, crashing sooner or later. Most commonly, the mistake is that an array extends only from 0..n-1. Declaring an array of size n does not allow the nth array element to be accessed or modified. A common form of this error is:

    int arr[MAX];
    ...
    for (int i = 1; i <= MAX; i++)  // Bug
        printf("%d\n", arr[i];

This code demonstrates the misconception that arrays start at 1 and extend to the declared size. The correct for loop starts at 0 and use < instead of <=, as below:

    for (int i = 0; i < MAX; i++)   // Correct

You might have read that C++ allows pointers to hold values one past the end of an array (i.e., hold the address &arr[MAX]). However, do not be confused into thinking that it is then legal to dereference these pointers to access or modify the value they point to. Pointers are allowed to hold such values only for the purposes of pointer comparisons.

Integer Division and remainder by zero

The division by zero error is something you learn early in programming. There's an issue with both integer division and floating-point division by zero.

Another less obvious issue is that the modulo or remainder operator (i.e. the binary % operator) also has the same problem. The division of the % operator cannot be zero.

The / and % operators officially have "undefined behavior" when their second operand, the divisor, is zero. On some machines this will raise an arithmetic exception that can be trapped by a signal handler; on other machines it is ignored, but the result is undefined.

Mathematical Range Tests with Relational Operators

A common error made by novice programmers is to assume that programming language operators are like mathematical notation. For example, although x ≤ y ≤ z is valid mathematical notation, the equivalent programming expression is erroneous:

    if (x <= y <= z)   // Bug

Unfortunately, such uses rarely cause compilation errors because of the flexibility in C++ expressions to mix operators freely. Such expressions will compile and run, but won't produce the expected results. For example, the test "x <= y <= z" will actually compare the value of z with the 0 or 1 (i.e., false or true) result of the x<=y relational operation. The left associativity of <= ensures this evaluation order.

Even worse is the confusion between = and == with 3 operands. The 3-way assignment is legal, and gets correctly processed from right-to-left:

    x = y = 2;    // Assignment

However, the 3-way equality test is not valid:

    if (x == y == 2)   // Bug

Equality tests on string constants and arrays

Applying either == or != to string constants is usually an indication of a novice's misunderstanding about strings. Consider the test expression:

    if (str == "YES")  // Bug

This does not test whether the string equals YES. Instead, it tests whether the address of str equals the address of where the string constant "YES" is stored, and will usually evaluate to false.

Applying the == or != operators to array variables is often an indication of an error. In particular, if the array is an array of char, this is probably the indication of a misunderstanding about how to test equality of strings. The correct method for comparing strings is to call the "strcmp" function or use the C++ string types properly.

Array Index Off-by-One Error

A common example of off-by-one involves problems with array indices. Remember that arrays indices range from 0..n-1, and not 1..n. When counting elements in an array, there are n elements, but the highest index is n-1. If the size is confused with the highest index, the program is off-by-one.

A common example of a one-off error occurs in the use of arrays. Common style for setting or accessing all elements in an array is to use a for loop similar to:

    for (i = 0; i < n; i++)
        a[i] = 0;

There are two ways this can go wrong — i can be initialized to 1 instead of 0, or the < operator can be mistakenly replaced with <=. In the first case, the zeroth array element is missed. In the second case, the array element a[n] is accessed, often causing a crash because it is an array out-of-bounds memory access error.

Commas in Multi-Dimensional Arrays

A common error made by novice programmers is using comma syntax for declaring multidimensional arrays:

   int arr[10, 20];   // Error

Although some compilers will warn about such uses, there will be no warning on some compilers. The problem is that the comma is treated as a "comma operator" that is evaluating the constant expression. Therefore, the above declaration will be treated by the compiler as:

    int arr[20];

Any use of arr will probably be erroneous:

    x = arr[i, j];    // Error

Again, this might be treated as a one-dimensional array access with the comma expression "i,j" as the index, which is equivalent to:

    x = arr[j];

Therefore, on some poor compilers that do not warn about commas in constant expressions this error can lead to very strange run-time behavior.

The valid C++ syntax uses two sets of square brackets rather than commas:

    int arr[10][20];   // Correct
    x = arr[i][j];        // Correct

Prefix Increment Off-by-One Error

Another example of off-by-one is whether to increment before or after an operation (i.e. prefix or postfix increment). Consider the for loop to copyalinked list into an array:

    int count = 0;
    for (p = head; p != NULL; p = p->next)
        a[++count] = p->data;    // INCORRECT

This is incorrect as the zeroth array element is missed. The problem is the use of the prefix ++ operator causing count to be incremented before its value is used for the array index. The correct statement uses post-increment:

    a[count++] = p->data; // CORRECT

Bitshifting Two

If you want a power-of-two, you need to use the left shift operator. But it's not like this:

    int eight = 2 << 3;  // Fail!

The correct bitshift code uses "1" not "2":

    int eight = 1 << 3;  // Correct

Bitshifting Signed Integers

Actually, this is still somewhat wrong:

    int eight = 1 << 3;  // Correct?

This will still work, but it's getting risky because "int" is shorthand for "signed int". Bit shifting operators and negatives don't mix, and shifts should always be operating on unsigned types. Safer code is:

    unsigned int eight = 1 << 3;  // Better?

Actually, no, this code is not really any better. The "<<" operator is not actually doing an unsigned operation yet. The above code does a signed "<<" operation and then converts from signed int to unsigned int at the assignment operator. So, it's still a complicated mess of both signed and unsigned arithmetic.

We want the "<<" operator to be doing its work in unsigned types. An easy correction is to use "1u" instead of "1" to make the constant unsigned (or "3u" would work, too, or both):

    unsigned int eight = 1u << 3;  // Better

More C++ Bug Catalogs