Skip to the content of the web site.

Lesson 1.11: Logical operators

Previous lesson Next lesson


Suppose there are two or more conditions that result in the exact same consequence, or there are two or more conditions that are required for a specific consequence.

For example, consider the interval function defined as:

We will start by assuming that $a > b$. Thus, if $x < a$ or $x > b$, the function evaluates to 0, otherwise, if $x = a$ or $x = b$, the function evaluates to $\frac{1}{2}$,
otherwise, $a < x < b$ and the function evaluates to 1.

Currently, we would implement this function as

// Function declarations
double interval( double x, double a, double b );

// Function definitions
double interval( double x, double a, double b ) {
    assert ( a < b );

    if ( x < a ) {
        return 0.0;
    } else if ( x > b ) {
        return 0.0;
    } else if ( x == a ) {
        return 0.5;
    } else if ( x == b ) {
        return 0.5;
    } else {
        return 1.0;
    }
}

Note that the result in both the first and second consequent bodies is identical, as is the result of the third and fourth consequent bodies. Thus, should one not be able to define a condition that allows either of two or more Boolean-valued statements to be true?

After all, in English, we would just say:

  • If $x < a$ or $x > b$, return $0$.
  • If $x = a$ or $x = b$, return $\frac{1}{2}$.
  • Otherwise, $x > a$ and $x < b$, so return $1$.

Programming languages have means of implementing such conditions, and C++ is no exception:

EnglishC++
$x < a$ or $x > b$(x < a) || (x > b)
$x = a$ or $x = b$(x == a) || (x == b)
$x > a$ and $x < b$(x > a) && (x < b)

Notice that $a < x < b$ is the same as saying that both $a < x$ and $x < b$.

We can now rewrite our function as follows:

// Function declarations
double interval( double x, double a, double b );

// Function definitions
double interval( double x, double a, double b ) {
    assert ( a < b );

    if ( (x < a) || (x > b) ) {
        return 0.0;
    } else if ( (x == a) || (x == b) ) {
        return 0.5;
    } else {
        assert( (x > a) && (x < b) );
        return 1.0;
    }
}

You will note that we use parentheses to group the statements.

Note that we can now re-implement the maximum-of-three function using multiple conditions:

// Function declarations
double max( double x, double y, double z );

// Function definitions
// 
// Return the maximum of the three arguments 'x', 'y', 'z'
double max( double x, double y, double z ) {
    if ( (x >= y) && (x >= z) ) {
        // If 'x' is the maximum value, return it
        return x;
    } else if ( y >= z ) {
        // As 'x' is not the maximum value, either 'y' or
	// 'z' is the maximum value, os if 'y' is larger
	// than 'z', 'y'
        assert( (y > x) && (y > z) );
        return y;
    } else {
    	// Otherwise, 'z' must be the largest
        assert( (z > x) && (z > y) );
        return z;
    }
}

The binary operators of logical OR and logical AND

The || binary operator is called the logical OR operator, and a || b evaluates to true if either a or b is true, and it evaluates to false otherwise.

The && binary operator is called the logical AND operator, and a && b evaluates to true if both a and b are true, and it evaluates to false otherwise.

Describing these operators in English can be unnecessarily long-winded, and there are more concise ways of describing such behavior using truth tables. Before we introduce truth tables, let us review your addition and multiplication tables, which you have by now already memorized:

+0123456789101112
00123456789101112
112345678910111213
2234567891011121314
33456789101112131415
445678910111213141516
5567891011121314151617
66789101112131415161718
778910111213141516171819
8891011121314151617181920
99101112131415161718192021
1010111213141516171819202122
1111121314151617181920212223
1212131415161718192021222324
×0123456789101112
00000000000000
10123456789101112
2024681012141618202224
30369121518212427303336
404812162024283236404448
5051015202530354045505560
6061218243036424854606672
7071421283542495663707784
8081624324048566472808896
90918273645546372819099108
100102030405060708090100110120
110112233445566778899110121132
1201224364860728496108120132144

The operator is placed in the top-left-hand corner, and all possible first operands of interest are written down the first column, and all second operands of interest are listed across the first row.

We can do the same for binary operators, except now the only two possible values of the operands are true and false, so the tables are much simpler:

||truefalse
truetruetrue
falsetruefalse
&&truefalse
truetruefalse
falsefalsefalse

Another approach that is now possible, given how few possible combinations of operands there are, is to list all possible pairs of values

xyx || yx && y
truetruetruetrue
truefalsetruefalse
falsefalsefalsefalse
falsetruetruefalse

Having multiple conditions that may be true

If there are multiple conditions that may be true, then it is possible to use a sequence of logical OR operators:

double unit_square_or( double x, double y );

// Return 1 if the pair (x, y) is in the unit square
// [-1, 1] x [-1, 1] and 0 otherwise.
double unit_square_or( double x, double y ) {
    if ( (x < -1) || (y < -1) || (x > 1) || (y > 1) ) {
        return 0.0;
    } else {
	return 1.0;
    }
}

We do not have to state which expression expression is evaluated first, as the logical OR operator is associative: (cond_1 || cond_2) || cond_3 always evaluates to the exact same value as does cond_1 || (cond_2 || cond_3), and thus the order is not relevant. This is the same with addition: $(x + y) + z = x + (y + z)$ for all values of $x$, $y$ and $z$, so we just write $x + y + z$ without ambiguity. Similarly, we may write cond_1 || cond_2 || cond_3 without worry of ambiguity.

In summary: given a sequence of logical OR operators, if any one of the operands is true, then the entire logical expression evaluates to true, otherwise, all operands are false, so the entire logical expression evaluates to false.

Having multiple conditions that may be true

If there are multiple conditions that must be true, then it is possible to use a sequence of logical AND operators:

double unit_square_and( double x, double y );

// Return 1 if the pair (x, y) is in the unit square
// [-1, 1] x [-1, 1] and 0 otherwise.
double unit_square_and( double x, double y ) {
    if ( (x >= -1) && (y >= -1) && (x <= 1) && (y <= 1) ) {
        return 1.0;
    } else {
	return 0.0;
    }
}

As with the logical OR operator, the logical AND operator is associative, so we may write cond_1 && cond_2 && cond_3 && cond_4 without any parentheses without any concern with respect to ambiguity:

In summary: given a sequence of logical AND operators, if all of the operands are true, then the entire logical expression evaluates to true, otherwise, at least one operand is false, so the entire logical expression evaluates to false.

Combining logical AND and OR operators

We have previously stated that logical OR and logical AND are associative, so there is no need to use parentheses to clarify statements such as

cond_1 || cond_2 || cond_3 || cond_4 || cond_5
cond_1 && cond_2 && cond_3 && cond_4 && cond_5

This is not the case with combinations of logical AND or OR operator: the two statements (cond_1 || cond_2) && cond_3 and cond_1 || (cond_2 && cond_3) evaluate to different results depending on the values they take. This can be shown with the following truth table, where we list all possible combinations of the values of the three operands and then proceed to determine the value of both logical statements:

xyzx || yy && z(x || y) && zx || (y && z)
truetruetruetruetruetruetrue
truetruefalsetruefalsefalsetrue
truefalsefalsetruefalsefalsetrue
truefalsetruetruefalsetruetrue
falsefalsetruefalsefalsefalsefalse
falsefalsefalsefalsefalsefalsefalse
falsetruefalsetruefalsefalsefalse
falsetruetruetruetruetruetrue

For example, if the first two conditions are true but the third is false, (cond_1 || cond_2) && cond_3 evaluates to false while cond_1 || (cond_2 && cond_3) evaluate to true. Thus, parentheses are vitally important to inform the reader of what you as a programmer meant: do not rely on cond_1 || cond_2 && cond_3 to be understood correctly: emphasize whether which you meant by using parentheses.

Short-circuit evaluation

One nice feature of a sequence a logical OR or a logical AND operation is that half the time, knowing the value of the first operand means that the result of the second operand need not even be calculated.

For example, given cond_1 || cond_2, if cond_1 evaluates to true, then regardless of the value of cond_2 (true or false), the entire logical expression must evaluate to true.

Similarly, given cond_1 && cond_2, if cond_1 evaluates to false, then regardless of the value of cond_2 (true or false), the entire logical expression must evaluate to false.

On the other hand, if cond_1 evaluates to false, the value of cond_1 || cond_2 depends on the value of cond_2; just like if cond_1 evaluates to true, the value of cond_1 && cond_2 also depends on the value of cond_2.

Recall that a computer must evaluate each operand of a binary logical operation separately, so it is quite reasonable to allow the computer to program not bother computing the second expression if the truth value can be determined from the first.

This is called short-circuit evaluation, and is quite common in programming languages. This is a very specific characteristic of logical AND and OR operators that you must learn separately from most other aspects of the C++ programming languages, as many of the characteristics of the language are shared across similar concepts. Short-circuit evaluation is a behavior that is very specific to logical AND and logical OR operators.

For example, in the above bivariate unit_square functions, suppose we call the function with the arguments unit_square_*( 1.5, -0.7 ). In the case of unit_square_or:

double unit_square_or( double x, double y ) {
    if ( (x < -1) || (y < -1) || (x > 1) || (y > 1) ) {
        return 0.0;
    } else {
	return 1.0;
    }
}

we would see that the first two operands evaluate to false, so the program must continue to evaluate x > 1, which evaluates to true. Therefore, the entire logical OR operation must be true, so there is no point in testing if y > 1. Instead, the function will immediately jump to the consequent body and return 0.0.

With the case of unit_square_and:

double unit_square_and( double x, double y ) {
    if ( (x >= -1) && (y >= -1) && (x <= 1) && (y <= 1) ) {
        return 1.0;
    } else {
	return 0.0;
    }
}

both the first two operands evaluate to true, so the program must continue to evaluate x <= 1, which is false, and therefore the entire logical AND operation must be false, so the program will immediately jump to the alternative body and return, again, 0.0.

Note that in either case, if the point $(x, y)$ is inside the unit square, all the conditions must be evaluated before it is determined that the point is in the square. If you wanted to make the code easier to read, you could author either of the following two alternatives, but in either case, it is still only hiding an additional call to a conditional test inside the abs function:

double unit_square_or( double x, double y ) {
    if ( (abs( x ) > 1) || (abs( y ) > 1) ) {
        return 0.0;
    } else {
	return 1.0;
    }
}

double unit_square_and( double x, double y ) {
    if ( (abs( x ) <= 1) && (abs( y ) <= 1) ) {
        return 1.0;
    } else {
	return 0.0;
    }
}

Logical negation

As well as saying that $x > y$, it is also possible to say that it it is not true that $x \le y$. If a logical statement is true then the negation of that statement must be false: If false that the sky is pink, then it is true that the sky is not pink.

In English, to indicate the negation of a logical expression, we generally use the word not. In C++, we can negate an expression cond by writing !( cond ) where ! is logical negation or the logical NOT operator.

In a conditional statement, the statement being true is the same as its negation being false. Thus, the following two conditional statements give the same result:

	if ( abs( x ) < 0.001 ) {
	    std::cout << "'x' is small." << std::endl;
	} else {
	    std::cout << "'x' is not small." << std::endl;
	}
	if ( !( abs( x ) < 0.001 ) ) {
	    std::cout << "'x' is not small." << std::endl;
	} else {
	    std::cout << "'x' is small." << std::endl;
	}

Complementary operators

You will recall that indicated that, for example, < and >= are complementary operators because if for a given set of operands, if one evaluates to true, the other must evaluate to false, and vice versa. Thus, the following are equivalent (meaning, they both must give the same result for the same $x$ and $y$):

x < y and !( x >= y)
x <= y and !( x > y)
x != y and !( x == y)
x == y and !( x != y)
x >= y and !( x < y)
x > y and !( x <= y)

Each of these is obvious: if $x$ is less than $y$, this is the same as $x$ being not greater-than or equal to $y$. In your courses on discrete mathematics and logic, you will also observe that

!(cond_1 || cond_2) and !( cond_1 ) && !( cond_2 )
!(cond_1 && cond_2) and !( cond_1 ) || !( cond_2 )

are always the same:

  • If it is false that it is either cold or wet outside, then it is false that both not cold and not wet outside.
  • If it is false that it is both cold and wet outside, then it is false that it is either not cold or not wet outside.

Negation is often negating the result of a Boolean-valued function:

bool divisible_by( int m, int n ) {
    if ( (m % n) == 0 ) {
        return true;
    } else {
        return false;
    }
}

Now we can do the following:

    // If 2 divides 'm', then 'm' must be even...
    if ( divisible_by( m, 2 ) ) {
        // Do something with 'm' because it is even
    }

    // If 2 does not divides 'm', then 'm' must be odd...
    if ( !divisible_by( m, 2 ) ) {
        // Do something with 'm' because it is odd
    }

Previous lesson Next lesson