Compound Conditions
So far, we know how to represent a series of potential cases in our C++ code using the if
block.
We also learned how we can write two mutually exclusive cases by utilizing the if/else
construct.
Then, we saw how to combine boolean expressions together via the logical operators.
Further on, it is very common for programmers to be faced with three or more mutually exclusive cases.
We can effectively deal with this situation by a new concept: the if/else if/else
statement.
Compound Conditional Statements
if (/* boolean condition */)
{
// The code in here executes only if the condition evaluates to true
}
else if (/* boolean condition */)
{
// The code in here executes only if the previous condition evaluates to false
// and the this condition evaluates to true
}
else
{
// This code in here executes if and only if all of the previous conditions
// evaluated to false
}
The if/else if/else
statement can represent multiple mutually exclusive cases.
The cases' conditions are evaluated from top to bottom, and if any of them
are true then it will execute that case's code then jump out of the entire structure.
If none of the boolean conditions evaluate to true, then the else
block will
finally be executed. Thus, all of these cases in entire chain are mutually exclusive.
The else
is totally optional, and there can be any number of else if
blocks.
In fact, you may realize that the if/else
discussed earlier is the case where
there are zero else if
's.
Thus, we can now see how all of if
, if/else
, and if/else if/else
are really
just one greater system that we call the if statement. An if statement
always starts with an if
block, optionally followed by any number of else if
blocks, and optionally followed by an else
block.
You should only combine cases into additional else if/else
clauses when they
are mutually exclusive. If you have multiple sets of independent cases where
you want multiple to potentially execute, you should keep them as separate if statements.
Let's use this new tool to upgrade our program even further.
Reinventing the Oracle
More regulations have been passed down from above. Now, there is a new license system being put in place. Drivers shall be segmented into different classes of license by their age. The rules are as follows:
- Drivers between the ages of
18
and21
inclusive are given a Class C license - Drivers between the ages of
22
and30
inclusive are given a Class B license - Drivers between the ages of
31
and50
inclusive are given a Class A license - Drivers between the ages of
51
and64
inclusive are given a Class C license - Drivers outside of these age ranges cannot receive a driver's license.
#include <iostream>
int main() {
std::cout << "Welcome to the Driver's License Oracle 4000\n";
int year_of_birth;
std::cout << "Please enter your year of birth: ";
std::cin >> year_of_birth;
// Year 2022 at the moment of writing this lesson
int age = 2022 - year_of_birth;
if ((age >= 18 and age <= 21) or (age >= 51 and age <= 64)) {
std::cout << "You can legally get a Class C driver's license\n";
} else if (age >= 22 and age <= 30) {
std::cout << "You can legally get a Class B driver's license\n";
} else if (age >= 31 and age <= 50) {
std::cout << "You can legally get a Class A driver's license\n";
} else {
std::cout << "You cannot legally get a driver's license because you are not between 18 and 64 years old\n";
}
}
Analyzing this new if statement should hopefully be straightforward. Our first case, for Class C drivers,
has two conditions under which it it will be issued: the person is between 18 and 21 years old,
or they are between 51 and 64 years old. So, we use a compound condition to build this logic by combining
two and
expressions with a single or
operator. Thus, we are separetly checking the two ranges
simultaneously.
Now, you could certainly do the Class C case without the or
operator by putting the second and
expression
into a dedicated else if
clause. However, because they both end up executing the same code (giving the user
a Class C license), it's best to combine them into a single condition to avoid repeating the code.
The general practice of avoiding repetition is called Don't Repeat Yourself aka DRY, and it's a very good
philosophy to keep in mind.
Following the DRY principle will lead to more maintainable, more readable code with fewer chances for bugs.
Back to the main topic at hand, the other cases are clearly laid out with else if
clauses.
We build each of these using an and
operator to build a lower bound and upper bound for each of the cases.
Finally, we use the else
clause as the "catch-all" to reject anyone whose ages didn't fall into the predefined boundaries.
Using variables to reduce repetition
It may seem odd that after all that talk of not repeating yourself, that we have nearly the same line of code repeated three times.
Notice how the std::cout << "..."
is almost identical for all of Class A, B, and C licenses — only a single letter is different
in each of the cases. We can use this to our advantage by creating a variable that stores a single character.
Then, we change our if statements to assign this variable the respective license class. Finally, we output
the final message combined with the license variable we created.
#include <iostream>
int main() {
std::cout << "Welcome to the Driver's License Oracle 4500\n";
int year_of_birth;
std::cout << "Please enter your year of birth: ";
std::cin >> year_of_birth;
// Year 2022 at the moment of writing this lesson
int age = 2022 - year_of_birth;
char license_class = 'X'; // X is chosen as a default value to signal if we miss one of our cases
if ((age >= 18 and age <= 21) or (age >= 51 and age <= 64)) {
license_class = 'C';
} else if (age >= 22 and age <= 30) {
license_class = 'B';
} else if (age >= 31 and age <= 50) {
license_class = 'A';
} else {
std::cout << "You cannot legally get a driver's license because you are not between 18 and 64 years old\n";
}
std::cout << "You can legally get a Class " << license_class << " driver's license\n";
}
The code above does exactly what we described. We created a char
variable to hold our license class,
then we used the if statements to assign the respective class to that variable. However, this code
will not work as intended!
Look back to the Anatomy of an if statement,
and notice the comment underneath the entire if statement — it says
"The code out here executes regardless of whether the condition is true or false."
So, the code will work correctly if we can legally get a driver's license. If we input 2000
as our
birth year, then it will print out You can legally get a Class C driver's license
as expected.
But, if we say we were born in 2015
, it will print out:
You cannot legally get a driver's license because you are not between 18 and 64 years old
You can legally get a Class X driver's license
... which is incorrect! It's printing out both of the cases because we moved the legal output to outside of the if statement.
What we need to do is create then another if statement that checks to see if we gave the user a valid license or not.
We can easily do this because of how we set 'X'
to be the default license value. This means that anyone that
did not receive a license will still have an 'X'
as the value of license_class
, and we can check this with a condition.
#include <iostream>
int main() {
std::cout << "Welcome to the Driver's License Oracle 4500\n";
int year_of_birth;
std::cout << "Please enter your year of birth: ";
std::cin >> year_of_birth;
// Year 2022 at the moment of writing this lesson
int age = 2022 - year_of_birth;
char license_class = 'X'; // X is chosen as a default value to signal if we miss one of our cases
if ((age >= 18 and age <= 21) or (age >= 51 and age <= 64)) {
license_class = 'C';
} else if (age >= 22 and age <= 30) {
license_class = 'B';
} else if (age >= 31 and age <= 50) {
license_class = 'A';
} // This else statement was removed to make the print a part of the following if instead
if (license_class != 'X') {
std::cout << "You can legally get a Class " << license_class << " driver's license\n";
} else {
std::cout << "You cannot legally get a driver's license because you are not between 18 and 64 years old\n";
}
}
You can see here how we've made a new if statement that checks license_class != 'X'
. This serves as the conditional
statement for choosing whether we gave a license to the user or not; then, we dispatch the control flow to
the proper print statement depending on the answer to that condition.
We have now reenabled the mutual exclusion of these two outputs.
By using variables and applying the DRY principle, you can see that we reduced the repetition of our code. This has made it more readable, more maintainable, and easier to debug.
Nesting conditional statements
The inside of an if statement is nothing special. You can put any valid executable code inside of an if statement's body,
just like how all the code inside the body of main
is valid executable code. Thus, it should be easy to "nest"
if statements by putting one inside of the body of another.
if (/* Condition 1 */) {
// Code here gets run if Condition 1 is true
// Since this if statement is inside another if statement,
// it only gets evaluated if the outer if statement's body is entered;
// ie., only if Condition 1 is true
if (/* Condition 2 */) {
// Code here gets run if Condition 2 is true
} else if (/* Condition 3 */) {
// Code here gets run if Condition 2 is false, and Condition 3 is true
}
// Code here gets run regardless of whether Condition 2 or 3 are true,
// however note this is still inside the body of the outer if statement!
} else {
// Code here gets run if Condition 1 is false
}
// Code out here gets run no matter the outcome of the above if statement
Here we can see a stubbed out example of what a nested if statement may look like in the wild. Here, we have one outer if/else statement, with an if/else if statement inside of the outer one's body.
When the program runs this code, the first step it performs is checking the result of Condition 1. If it is true, it will step into its body and execute the code in there. Else, if Condition 1 is false, it will jump into the else's body and execute that code. After this whole process is done, it will then exit the entire conditional statement and continue execution below.
Notice here that, in the case of Condition 1 being true, part of its body is another if statement. So, this means that it will again make a true/false decision. So, if Condition 1 is true, it will continue until it reaches the nested if statement. Then, it checks Condition 2. If that is true, it executes that body. Else, if Condition 2 is false, it checks Condition 3. If Condition 3 is true, it executes that body.
Take note of how similar these processes are overall. Nothing really changed when analyzing the behavior
of the outer if statement versus the inner if statement. If statements are extremely modular in this way —
their behavior is the same in any context... at the beginning of main
, at the end of main
, inside
another if statement, inside two if statements, inside an else body, etc.
Let's use this new tool to upgrade our Oracle even further:
The Overseer Council of Transportation Safety has issued a new requirement: those with two or more accidents are not eligble for any class license regardless of their age.
#include <iostream>
int main() {
std::cout << "Welcome to the Driver's License Oracle 5000\n";
int year_of_birth;
std::cout << "Please enter your year of birth: ";
std::cin >> year_of_birth;
int num_crashes;
std::cout << "Please enter the number of crashes you have had: \n";
std::cin >> num_crashes;
if (num_crashes < 2) {
// Year 2022 at the moment of writing this lesson
int age = 2022 - year_of_birth;
char license_class = 'X'; // X is chosen as a default value to signal if we miss one of our cases
if ((age >= 18 and age <= 21) or (age >= 51 and age <= 64)) {
license_class = 'C';
} else if (age >= 22 and age <= 30) {
license_class = 'B';
} else if (age >= 31 and age <= 50) {
license_class = 'A';
}
if (license_class != 'X') {
std::cout << "You can legally get a Class " << license_class << " driver's license\n";
} else {
std::cout << "You cannot legally get a driver's license because you are not between 18 and 64 years old\n";
}
} else {
std::cout << "You cannot legally get a driver's license because you have " << num_crashes << " accidents\n";
}
}
You can see here how we have moved a large chunk of the previous code into the body of the numsCrashes
if statement.
This means that we will only perform that code if and only if the user has less than two crashes.
Otherwise, if the user has two or more crashes, the system prints out the requisite notification that they cannot
get a driver's license.
Now take notice again how we are repeating the same message twice in our code. This means there is another opportunity to apply the DRY principles and simplify our code for future maintenance. Let's take a look:
Applying DRY principles once more
#include <iostream>
int main() {
std::cout << "Welcome to the Driver's License Oracle 5500\n";
int year_of_birth;
std::cout << "Please enter your year of birth: ";
std::cin >> year_of_birth;
int num_crashes;
std::cout << "Please enter the number of crashes you have had: \n";
std::cin >> num_crashes;
char license_class = 'X'; // X is chosen as a default value to signal if we miss one of our cases
std::string illegal_reason;
if (num_crashes < 2) {
// Year 2022 at the moment of writing this lesson
int age = 2022 - year_of_birth;
if ((age >= 18 and age <= 21) or (age >= 51 and age <= 64)) {
license_class = 'C';
} else if (age >= 22 and age <= 30) {
license_class = 'B';
} else if (age >= 31 and age <= 50) {
license_class = 'A';
} else {
illegal_reason = "you are not between 18 and 64 years old";
}
} else {
illegal_reason = "you have " + std::to_string(num_crashes) + " accidents";
}
if (license_class != 'X') {
std::cout << "You can legally get a Class " << license_class << " driver's license\n";
} else {
std::cout << "You cannot legally get a driver's license because " << illegal_reason << "\n";
}
}
We can see that this code has been improved for multiple reasons. Firstly, we are no longer repeating ourselves, meaning that future maintenance is significantly easier in case the Overseer Council decides to add more regulations.
Secondly, we have now made a clean split between our "logic code" and the "display code".
If you intersperse your console i/o commands with your logical computation commands,
it can make the code become very confusing very quickly. In the Oracle 5000 program, all of our logic
for determining the license_class
and illegal_reason
are in one if statement, and the
code for displaying the results ot the user are in a separate if statement.