3.3. Logical Expressions

Time: 00:06:30 | Download: Large, Large (CC), Small | Streaming, Streaming (CC) | Slides (PDF)
Review

Logical expressions are a fundamental part of control statements, and programmers form them with combinations of two kinds of operators:

  1. Relational operators
  2. Logical operators

It's not possible to understand or to effectively use control statements without a thorough understanding of logical expressions and the operators that form them.

Relational Operators

Relational operators form complex, Boolean-valued expressions by comparing or relating two numeric sub-expressions. Being Boolean-valued means that the result of the expression is constrained to one of two values: true or false. The following table lists all the relational operators.

Operator Meaning
== Equal to
!= Not equal to
< Less than
<= Less than or equal to
> Greater than
>= Greater than or equal to
C++ relational operators. Relational operators compare two sub-expressions and form a more complex Boolean-valued expression. They are left-associative (evaluated left to right) and have lower precedence than the arithmetic operators.

As relational operators compare two values, they are all binary (i.e., they require two operands), and, like most binary operators, they are left-associative (evaluated left to right). Each operand is an expression, a simple or arbitrarily complex one. However, "testing" the relation between two constants has little value in a program, so at least one operand is usually a variable or a more complex expression.

Programmers typically use relational operators in control statements (like loops and if-statements), which are the subjects of the following sections. C++ control statements are much the same as those you are familiar with in Java from your earlier studies.

if (counter == 0) . . .
if (counter != x + y) . . .
if (m / n > x * y) . . .
if (max < sqrt(x)) . . .
if (counter >= pow(x, y)) . . .
if (counter <= 0) . . .
Examples of simple relational expressions. Both the left- and right-hand sides of each operator is an expression. The examples assume that the variables counter, x, and y are all defined. The result of each logical expression is true or false.

Logical Operators

Boolean Operators

C++ and Java both support two kinds of Boolean operators: logical and bitwise. The nature of programming can cause programmers to become pathologically precise: they take great care to avoid confusion by writing "logical-AND" or "bitwise-AND" to distinguish between the two kinds of operators. The bitwise operators were introduced in the supplemental section of Chapter 2, while the logical operators are the subject of the following section. For simplicity, the words AND, OR, or NOT, written with all upper case letters, refer to the logical Boolean operators.

C++ provides the same three logical operators as Java; two of the operators are binary, while one is unary. The operands for all three operators are Boolean, and the result of the operation is a Boolean value. All of the operators are left-associative.

Operator Meaning Alternate1
! Logical NOT not
&& Logical AND and
|| Logical OR or
C++ logical operators. The logical operators (from highest to lowest precedence) combine two Boolean-valued sub-expressions (its left and right-hand operands) to form a Boolean-valued result.

1 The alternative versions (which are case-sensitive) were added to support small character sets that do not include the traditional characters. Some versions of C++ provide alternative versions of the relational operators. See Alternative operators for more information.

Caution

Although the acceptance of the alternative operator representations is wide and growing, the alternatives are NOT universally supported by all compilers (specifically, Visual Studio). I recommend using only the operator versions generally and the alternatives only when necessary.

if (counter != x + y && m / n > x * y) . . .
if (max < sqrt(x) || counter >= pow(x, y)) . . .
if (!done) . . .
Simple logical expressions. Logical operators require Boolean-valued operands and produce a Boolean-valued result. This observation suggests that it is possible to create logical expressions that consist of long sequences of Boolean-valued sub-expressions and logical operators. The examples assume that the variables counter, x, y, m, max, and done are defined. Each logical expression is demonstrated with an if-statement but will also work with loops and other branch statements.

The logical operators have a limited domain (input) and range (output), which makes it easy for us to enumerate all the possible combinations of inputs and outputs. Summarizing all possible combinations in a truth table is common.

E1 E2 E1 && E2
false false false
false true false
true false false
true true true
E3 E4 E3 || E4
false false false
false true true
true false true
true true true
E5 ! E5
false true
true false
(a) (b) (c)
Truth tables for the logical operators. E1 through E5 represent arbitrarily complex Boolean-valued expressions, for example, m / m > x * y
  1. AND produces TRUE only when both operands are TRUE.
  2. OR produces FALSE only when both operands are FALSE.
  3. NOT toggles TRUE to FALSE and FALSE to TRUE.

Knowing how to interpret the results of these operators is essential to understanding the behavior of the logical operators as they appear in expressions, and understanding the behavior of the expressions is fundamental to correctly forming even the most basic control statements. So, if you have not memorized these truth tables, you should do so immediately.

Short Circuit Evaluation

From the tables above, we see that AND and OR produce TRUE in only one case each. We can use these observations to form clear, efficient control statements. The AND operation has a TRUE value only when both operands are TRUE. So, if the left-hand operand evaluates to FALSE, the value of the right-hand operand makes no difference. Similarly, OR produces a FALSE value only when both operands are FALSE. So, if the left-hand operand evaluates to TRUE, then the value of the right-hand operand is irrelevant.

E1 && E2   E3 || E4
E1 E2 Evaluated   E3 E4 Evaluated
True Yes   True No
False No   False Yes
Short circuit evaluation. C++ and Java both employ an evaluation scheme called short circuit evaluation when evaluating expressions using the AND and OR operators. Programs evaluate expressions using these operators from the left to the right and skip the evaluation of any subsequent sub-expressions as soon as the overall result is determined. Each En is an arbitrarily complex Boolean-valued expression, for example, a == b, a < b, or x + y > 10.
if (n != 0 && 100 / n > min)
	...
if (str != nullptr && str.length() > 0)
	...
(a)(b)
Short circuit examples. Short circuit evaluation allows programmers to write clear, secure, succinct operations.
  1. If n is 0, then evaluating 100 / n would cause a divide by 0 error. However, the careful arrangement of the sub-expressions coupled with short circuit evaluation prevents this error. That is, if n is 0, then n != 0 is false, and short circuit evaluation prevents the evaluation of 100 / n and the error caused by evaluating it.
  2. Calling a function or method through a null object is an error in both C++ and Java programs. If the variable str is a string object, programmers can use short circuit evaluation to prevent this error. To convert this example from C++ to Java, replace nullptr with null.

(As an aside, Java defines two logical AND and OR operator versions. One version provides short circuit evaluation while the other does not. However, I've never seen a textbook use the non-short circuit version beyond its demonstration; more importantly, I've never seen the non-short circuit version used in production code!)

Boolean Type and Values

The C programming language, the predecessor of both C++ and Java, does not define a Boolean data type, but C++ and Java do. C++ adds the bool type, and Java refines it and names it boolean (which is distinct from the Java wrapper class named Boolean). C++ and Java also add two related keywords: true and false. The discussions of the relational and logical operators above also apply to C, but how does C manage if it doesn't have a Boolean data type or the keywords to represent the two possible Boolean values? The answer to that question is at the center of an essential difference between C++ and Java.

In Java, boolean is a distinct data type that is unrelated to any numeric type. That means that true and false are unorderable and without a numeric representation. However, in C++, bool is just a syntactic candy coating (i.e., a synonym) for an int! Furthermore, true and false are just aliases for 1 and 0 respectively. Dealing with Boolean operations and values this way has some unexpected consequences.

  • Any non-0 value is treated as true
  • Any 0-equivalent value is treated as false
  • true and false can be ordered: true > false
if (n % 2)
	cout << "n is odd\n";
else
	cout << "n is even\n";
(a)(b)
Alternate representations of Boolean values.
  1. Any expression can serve as a Boolean expression! "0-equivalent" means a value treated like 0 regardless of the data type. For example, a program can assign the value nullptr to a pointer variable (Chapter 4) and then logically treat it as 0. Being able to order the Boolean constants is irrelevant.
  2. Interpreting 0 and non-0 as Boolean values suggests we can form control statements in C++ without using relational or logical operators. For example, you can use the mod operator to determine if a number is odd or even. If n is an integer (any size), n % 2 is one of -1, 0, and 1. It is ±1 when n is odd and 0 when it is even. C++ interprets -1 and 1 as true and 0 as false. While this allows us to write efficient code, it is also the source of a common programming error.

A Common Error: Assignment vs. Equality

Although using an arbitrary expression to control if-statements and loops is convenient, it also allows programmers to make an easy programming mistake.

int	counter;
	. . .
if (counter == 10)
	. . .;
int	counter;
	. . .
if (counter = 10)
	. . .;
(a)(b)
Confusing the assignment and equality operators. Given their similarity and their different uses in various programming languages, spreadsheets, and database systems, it's understandable that confusing the assignment and equality operators is common. Furthermore, how C++ defines true and false makes it difficult for the compiler to detect this common error.
  1. A correct test for equality.
  2. But what if the programmer inadvertently only writes one "=" character? The compiler processes this as an assignment operation embedded in the if-statement, which is syntactically correct. When the code runs, it stores the value of 10 into counter; then the assignment operator returns the value just stored, 10, which the operation treats as true. So the if-statement always "sees" the value 10 inside the parentheses, and any statement that is part of the true branch of the if-statement always runs.
(b) is an example of a logical error that the compiler does not detect. Java can detect this error at compile time in most cases - it fails in the case of a boolean variable.