Conditions
Conditions are expressions that evaluate to a boolean value — a
true or false value (true and false are C++ keywords,
representing the two possible values of a boolean expression or variable).
Simple conditions involve two operands, each of which can be a variable
or a literal value, and an operator, typically a comparison operator.
The comparison operators are shown below:
== |
true if and only if left operand is equal to right operand |
!= |
true if and only if left operand is not equal to right operand |
> |
true if and only if left operand is greater than right operand |
< |
true if and only if left operand is less than right operand |
>= |
true if and only if left operand is greater than or equal than right operand |
<= |
true if and only if left operand is less than or equal than right operand |
The examples below illustrate the use of conditions in an if statement:
if (price <= 0)
{
// Error condition
}
if (price > average_price)
{
// Apply some discount
}
if (age < 18)
{
// Error message -- not allowed to buy alcohol
}
if (age >= 65)
{
// Apply seniors' discount
}
The operands can be arbitrary expressions, like in the example shown below:
if (a*a + 2*b >= c / (d+1))
Boolean Variables
A condition may be given directly by a boolean variable. This should be quite
clear from the fact that a variable of type bool can only hold the
values true or false. In essence, a boolean variable is
an expression (a very simple expression) that evaluates to
true or false, so it is a
particular case of a condition.
We could take advantage of boolean variables in a situation where we need
to avoid repeated evaluation of a condition that is complicated and takes
many operations. For example, let's say that our program needs to evaluate
the following mathematical expressions:
x1 |
= |
a2 + b2
x2 − 2 (y12 + y22) + y2 |
x2 |
= |
a2 − c2
x2 − 2 (y12 + y22) + y2 |
x3 |
= |
2 a2 + 3 b2
x2 − 2 (y12 + y22) + y2 |
For each of the expressions, we need to check that we don't attempt a division by 0.
But we notice that for the three cases, we divide by the same value; so, evaluating
three times that complicated expression to compare it against zero is unnecessary. We
could evaluate the condition only once, and store the result (remember that the result
is a true / false value) in a boolean variable
that may be used later as a condition, as shown below:
const bool not_div_by_0 = x*x - 2*(y1*y1 + y2*y2) + y*y != 0;
if (not_div_by_0)
{
x1 = (a*a + b*b) / (x*x - 2*(y1*y1 + y2*y2) + y*y);
}
// other statements required before computing x2 and x3
if (not_div_by_0)
{
x2 = (a*a - c*c) / (x*x - 2*(y1*y1 + y2*y2) + y*y);
}
// other statements required before computing x3
if (not_div_by_0)
{
x3 = (2*a*a + 3*b*b) / (x*x - 2*(y1*y1 + y2*y2) + y*y);
}
The above is just a silly example — it would be better to evaluate
the result of the complicated expression in the denominator and check
if that value is different from zero; that way we would optimize the
computation of x1, x2, and x3, since the common part of the three
formulas is already pre-computed. Still, the above is intended as
an example of how to avoid repeated evaluation of a condition by
storing the result of the condition in a boolean variable; using
the value of the boolean variable as a condition is much more
efficient than having to re-compute the complicated expression.
An additional benefit is that it can help improve readability; the
same issues discussed for named constants apply in here — we are
giving a meaningful name to the otherwise long and complicated
condition, making it easier to read and understand.
We could also use the NOT operator (!), which negates the
value of the boolean expression:
const bool div_by_0 = x*x - 2*(y1*y1 + y2*y2) + y*y == 0;
if (! div_by_0) // we read this as: if not div_by_0
{
// .... etc.
The space between ! and div_by_0 is optional, but I
normally recommend to use it, since it increases readability — if we
omit it, the ! could be easily missed when quickly looking at
the expression.
Composite Conditions
Very often, we encounter situations where the condition can not be expressed as
the simple conditions from the previous section, just by comparing two values.
An example of such situations is testing if a number is within a given range
of values; for instance, testing if a number is between 0 and 10.
This example involves testing two conditions, and verifying if the two
conditions are simultaneously met. In other words, we want to test if the
number is greater than or equal to 0 and also less than or equal to 10.
To this end, we use logical operators to combine two conditions. In the above
example, we use the logical AND
operator — &&,
as shown below:
if (0 <= number && number <= 10)
Notice that, even though the first part may look a little strange, there is
nothing wrong with it — each operand around a comparison operator may be
an arbitrary expression, so there is nothing that prevents us from writing
a numeric constant on the left and a variable on the right. I wrote this
condition this way to make it look like the equivalent mathematical notation,
0 ≤ n ≤ 10.
However, notice that we can not write (0 <= number <= 10), because
the comparison operators (in this case, <=) require two compatible
expressions (in this case, two expressions that evaluate to a numeric value)
on each side. If we tried to write the expression in this compact form, the
second operator <= would not have a numeric expression on its left,
but a boolean expression, and we would be asking the program to
compare if a true/false value is less than or equal than 10.
Also, we can not write it the way we say it in English (or I presume, in
most spoken languages), which would be “if the number is greater than 0
and less than 10”. This works in spoken languages because the subject
in each sub-sentence is implicitly assumed to be the same. In programming
languages, things do not work that way — computers are too literal and
too structured for these kinds of linguistic subtleties.
The rule is simple: the logical AND operator requires two expressions
that are valid conditions on each side. If we write number >= 0 && <= 10,
the expression on the right of && is not a valid condition.
The same applies to the logical OR operator, which in C++ is written as two
pipe characters, as shown in the example below:
if (answer == 'y' || answer == 'Y')
In this example, we use a composite condition to check, in a case-insensitive
way, if the variable answer contains a y.
The NOT operator can also be used for arbitrary logical expressions;
it is not restricted to be used with boolean variables. For example,
the following if statement is perfectly valid:
if (! (answer == 'q' || answer == 'Q'))
The above could be seen as checking if the user did not select the
code to Quit the application. (Try writing the above expression
without using the NOT operator! You may be surprised by how complicated
it may be to get it right — if you don't believe me, try it! And do
make sure that you get it right!)
Short-circuit Evaluation
In C++, the logical operators && and || have an interesting
property: short-circuit evaluation. The language rules require that when
evaluating a composite condition, if the result from the first part (the
expression on the left of the logical operator) is sufficient to determine
the result of the condition, then the second part must not be evaluated.
This is a stronger requirement than simply an optimization issue; when
evaluating the condition such as 0 <= number && number <= 10, it
seems a reasonable optimization that if the number is negative, then as soon
as the first part of the condition is evaluated, we do not need to evaluate
the second part to know what is the final result — if the first part of
the condition evaluates to false, then the requirement that both
be true will also evaluate to false, and so, we do not need to
make the processor waste time in evaluating something that will not make a
difference in the result. The same applies to the logical OR operator
(||) when the first part of the expression evaluates to
true; at this point, the result of the whole condition is
known with absolute certainty, regardless of the second part of the
condition.
In C++, the rule goes beyond the above reasoning — it's not that the
compiler doesn't need to evaluate the second part in these cases; it is
not allowed to do it, and there are situations where we rely on
this requirement, such as the following:
In this case, we want to make sure that we only divide by b if
the value of b is not 0. In other languages where we don't have
short-circuit evaluation of logical expressions, the above would be
incorrect, since the compiler would still evaluate both conditions to
then apply a logical AND of the two results — but merely attempting
to evaluate the second part causes an error (division by 0). With
short-circuit evaluation, we know that a division by 0 can not be even
attempted, since the first condition evaluating to false (which
would happen if b is 0) prevents the compiler from
evaluating the second part.
The if-else Statement
The simple if statement covers the cases where we have a fragment
of code that should be executed depending on a condition. If we have a
situation where we have two possible actions, and we want to do one or the
other, depending on a given condition, we use the
if – else
statement.
The syntax for the if – else statement is:
if (condition)
{
// block 1
}
else
{
// block 2
}
In the above, if the condition is true, then block 1 is executed, and then
execution continues after the end of block 2; if the condition is false,
then block 2 is executed (block 1 is skipped), and then execution continues
after the end of block 2.
An extension of the above idea is the else if statement. Strictly
speaking, there is no such thing as an else if statement in C++, and
it is simply a nested if following the else part of the
first statement. The generic syntax is as follows:
if (condition 1)
{
// block 1
}
else if (condition 2)
{
// block 2
}
...
else if (condition N)
{
// block N
}
else
{
// Optional block to be executed if all of the
// previous conditions evaluated to false
}
Notice that only one of the blocks will be executed. This includes
the block corresponding to the optional else part — if one of
the previous blocks executes, then the rest is skipped, and execution
continues after the end of the else block, or after the last
else if block, in the cases where there is no else.
One example where the use of else if provides a convenient
solution is checking multiple ranges for a value. For example, if we
want to convert a final grade in a scale of 0 to 100 to a letter grade
(for example, 90 to 100 corresponds to an A, 80 to 89 is B, 70 to 79
is C, 60 to 69 is D, and less than 60 is F), we could use the following
fragment of code:
if (grade >= 90)
{
letter_grade = 'A';
}
else if (grade >= 80)
{
letter_grade = 'B';
}
else if (grade >= 70)
{
letter_grade = 'C';
}
else if (grade >= 60)
{
letter_grade = 'D';
}
else
{
letter_grade = 'F';
}
At each stage, the else part is executed only if the previous
conditions were false, so we implicitly have the double condition (for
example, when checking grade >= 80, we know that grade
is less than 90, since we are in the else part of the
if (grade >= 90)).
|