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:
Programming languages have means of implementing such conditions, and C++ is no exception:
English | C++ |
---|---|
$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 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:
+ | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
1 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
2 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
3 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
4 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
5 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
6 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
7 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
8 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
9 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
10 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 |
11 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 |
12 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 |
× | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
1 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
2 | 0 | 2 | 4 | 6 | 8 | 10 | 12 | 14 | 16 | 18 | 20 | 22 | 24 |
3 | 0 | 3 | 6 | 9 | 12 | 15 | 18 | 21 | 24 | 27 | 30 | 33 | 36 |
4 | 0 | 4 | 8 | 12 | 16 | 20 | 24 | 28 | 32 | 36 | 40 | 44 | 48 |
5 | 0 | 5 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 |
6 | 0 | 6 | 12 | 18 | 24 | 30 | 36 | 42 | 48 | 54 | 60 | 66 | 72 |
7 | 0 | 7 | 14 | 21 | 28 | 35 | 42 | 49 | 56 | 63 | 70 | 77 | 84 |
8 | 0 | 8 | 16 | 24 | 32 | 40 | 48 | 56 | 64 | 72 | 80 | 88 | 96 |
9 | 0 | 9 | 18 | 27 | 36 | 45 | 54 | 63 | 72 | 81 | 90 | 99 | 108 |
10 | 0 | 10 | 20 | 30 | 40 | 50 | 60 | 70 | 80 | 90 | 100 | 110 | 120 |
11 | 0 | 11 | 22 | 33 | 44 | 55 | 66 | 77 | 88 | 99 | 110 | 121 | 132 |
12 | 0 | 12 | 24 | 36 | 48 | 60 | 72 | 84 | 96 | 108 | 120 | 132 | 144 |
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:
|| | true | false |
---|---|---|
true | true | true |
false | true | false |
&& | true | false |
---|---|---|
true | true | false |
false | false | false |
Another approach that is now possible, given how few possible combinations of operands there are, is to list all possible pairs of values
x | y | x || y | x && y |
---|---|---|---|
true | true | true | true |
true | false | true | false |
false | false | false | false |
false | true | true | false |
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.
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.
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:
x | y | z | x || y | y && z | (x || y) && z | x || (y && z) |
---|---|---|---|---|---|---|
true | true | true | true | true | true | true |
true | true | false | true | false | false | true |
true | false | false | true | false | false | true |
true | false | true | true | false | true | true |
false | false | true | false | false | false | false |
false | false | false | false | false | false | false |
false | true | false | true | false | false | false |
false | true | true | true | true | true | true |
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.
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; } }
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; }
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:
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 }