Why this matters
Many checks ask whether a value lies between two limits. A password must be at least 8 characters and at most 64. A menu choice must be from 1 to 5. A room temperature must be from 5 to 30 degrees. In Python you can write 1 <= x <= 10, and it reads the way you would say it in English. In C++ the same text compiles and runs, but it gives the wrong answer for almost every input. C++ does not stop the program with an error. The program simply takes the wrong branch and keeps going.
This lesson covers one kind of condition: a value that must sit inside a range. The other ways to write conditions are in the rest of this module.
The idea: C++ reads two comparisons as two steps
A chain such as 1 <= x <= 10 is read from left to right, one operator at a time. C++ reads it as (1 <= x) <= 10.
The first step is 1 <= x. Each comparison in C++ gives a bool, which is a value that is either true or false. A bool is a very small number inside the computer. true counts as 1 and false counts as 0 when it is compared with a number.
The second step takes that bool and compares it with 10. So the second step compares either 0 or 1 with 10. Both 0 and 1 are at most 10. The second comparison is therefore always true, and so is the whole condition.
The value of x never affects the answer. The chain looks like a range test, but it is only a test that always passes.
Worked example 1: the chain that always passes
Read the program and predict which numbers print as "in range".
The output is:
0 is in range (chained)
1 is in range (chained)
5 is in range (chained)
10 is in range (chained)
11 is in range (chained)
15 is in range (chained)
-4 is in range (chained)Every number is reported as in range. The table below traces the two steps for four values, so you can see where each answer comes from.
| x | 1 <= x as a bool | As a number | (1 <= x) <= 10 |
|---|---|---|---|
| 5 | true | 1 | true |
| 11 | true | 1 | true |
| 0 | false | 0 | true |
| -4 | false | 0 | true |
Look at two of the wrong answers in detail.
- For
xequal to 15,1 <= 15istrue, which counts as 1. Then1 <= 10istrue. The test passes, so 15 is reported as in range. - For
xequal to 0,1 <= 0isfalse, which counts as 0. Then0 <= 10istrue. The test passes, so 0 is reported as in range.
The result is the same for -4 and every other whole number. This is not a rare bug. The condition cannot be false for any int, because the second step always gets 0 or 1 to compare with 10.
Worked example 2: the two-comparison form
The fix is to write two separate comparisons and join them with &&. The && operator means both sides must be true. Each comparison has the variable on one side and a plain number on the other.
Read the condition in words first. x is at least 1, and x is at most 10. Both parts must hold.
The output is:
0 is out of range
1 is in range
5 is in range
10 is in range
11 is out of range
15 is out of range
-4 is out of rangeThe two limits are both included. The number 1 is in range, and so is 10. The words "at least" map to >=, and "at most" map to <=.
Requirements are written in words, so it helps to keep a short list of the phrases and their operators.
| Words in the requirement | Operator | Example |
|---|---|---|
| at least, no less than | >= | x >= 1 |
| at most, no more than | <= | x <= 10 |
| more than, greater than | > | x > 1 |
| less than | < | x < 10 |
When you want to leave out one end, use the strict form. For example, x > 1 && x < 10 accepts 2 through 9 and rejects 1 and 10.
Worked example 3: a reusable check with a function
When the same range check appears in more than one place, put it in a function. The function gets the value and the two limits as parameters. A parameter is a named input that the function receives when it is called.
The output is:
in_range(5, 1, 10) = true
in_range(15, 1, 10) = false
key '7' is digit: true
rating 4.5 is in 4.0 to 5.0: trueThe same pattern works for any type that can be compared with <=. A char is compared by its code number, so '0' <= key && key <= '9' checks that key is one of the digit characters. A double works in the same way, with the caveat that decimal numbers are stored only approximately. The lesson on floating-point comparisons in the variables module explains that.
Notice the order of the parameters in in_range. The value comes first, then the low limit, then the high limit. Swapping them is easy to do, and the next section shows what happens.
Worked example 4: counting the values that pass
The first three examples checked one value at a time. Real programs often hold a list of values and need to know how many pass the check. The standard library has a function for this, ranges::count_if, in the <algorithm> header. It walks through the list, calls a test function on each item, and returns how many times the test returned true.
A vector is a resizable list from the standard library. The list below holds dice rolls. A die has faces from 1 to 6, so a roll of 0 or 7 is a mistake in the input.
The output is:
valid rolls: 3 of 5
3 ok
7 rejected
0 rejected
6 ok
1 okTwo details are new. The function is_valid_roll is passed by name to count_if, so the test is written once and used by both the count and the loop. The question mark in (is_valid_roll(r) ? " ok" : " rejected") is the ternary operator, which picks one of two values. It is covered in the ternary lesson of this module.
The test inside is_valid_roll is the same two-comparison form. A chained form would have passed 7 and 0 as valid, and the count would have been 5 of 5.
Worked example 5: limits that come from variables
Often the limits are not fixed numbers in the code. They are read from a settings file or chosen by the user, so they sit in variables. The check looks the same, but one new case appears. If the low limit is above the high limit, no value can pass. A good check reports that case as a separate problem instead of letting every value fail with the same message.
The output is:
players 9: outside the limits
players 4: ok
swapped: bad limits: low is above highThe first check handles a settings file that has the two numbers in the wrong order. The second check is the range test from this lesson, with both comparisons joined by &&. The "bad limits" message tells the reader which kind of problem happened, which is easier to fix than "outside the limits" for every value.
Checking the edges
Most range bugs happen at the ends of the range, not in the middle. An off-by-one mistake, such as writing > where >= belongs, changes only the value at one limit. So when you test a range, use the values just below the low limit, the low limit itself, the high limit, and the value just above the high limit.
For a range from 1 to 10 those values are 0, 1, 10 and 11. The program below prints them with the answer for each one.
The output is:
value in 1..10
0 no
1 yes
2 yes
9 yes
10 yes
11 noThe values 0 and 11 are the first ones outside the range, and 1 and 10 are the first ones inside. If the check were written with > at the low end, the value 1 would print no, and the table would show the mistake right away. Two values in the middle, such as 2 and 9, confirm that the check is not rejecting the whole range.
When the answer for a boundary value is not what the requirement says, look at the operator at that limit first.
Common mistakes
Mistake 1: writing the chain
The chained form compiles with no error. With the -Wall option turned on, g++ 15 reports two warnings. Here is the output of g++ -std=c++20 -Wall for the program in worked example 1, saved as block1.cpp.
block1.cpp: In function ‘int main()’:
block1.cpp:6:20: warning: comparison of constant ‘10’ with boolean expression is always true [-Wbool-compare]
6 | if (1 <= x <= 10) {
| ~~~~~~~^~~~~
block1.cpp:6:15: warning: comparisons like ‘X<=Y<=Z’ do not have their mathematical meaning [-Wparentheses]
6 | if (1 <= x <= 10) {
| ~~^~~~The first warning says the comparison with 10 is always true. The second warning names the pattern and says it does not mean what it looks like. Treat either one as a bug. Do not silence it.
Mistake 2: using || where && is needed
The outside of a range is "below the low limit or above the high limit". A student who wants to reject bad values might write this:
if (x >= 1 || x <= 10) {
cout << "valid\n";
}Every int is either at least 1 or at most 10, so the condition is always true. The program prints valid for 50, for -4, and for every other number. The correct check for "inside the range" uses &&. The correct check for "outside the range" uses || with the opposite comparisons: x < 1 || x > 10.
Mistake 3: swapping the limits
Calling in_range(5, 10, 1) passes 10 as the low limit and 1 as the high limit. No low limit can be greater than a high limit, so the check fails for every value.
The output is:
swapped limits: 0The number 0 is the way a bool prints without boolalpha. The function does not report a mistake. Check the argument order every time you call a function with more than one number.
Mistake 4: using the wrong operator at one end
If the lesson says "from 1 to 10 including both", the lower check must be >=, not >. With >, the value 1 is rejected.
The output is:
exclusive lower bound, one: 0Write the operator that matches the words in the requirement. The phrase "at least" is >=, and the phrase "more than" is >.
Mistake 5: a lower limit of 0 on an unsigned type
An unsigned int holds only zero and positive whole numbers. A lower limit of 0 is then always met, so the first comparison in the range check is always true. The program below was saved as unsig.cpp.
Compiled with -Wall -Wextra, g++ 15 reports this warning. The option -Wextra is separate from -Wall, so turn on both.
unsig.cpp: In function ‘int main()’:
unsig.cpp:6:15: warning: comparison of unsigned expression in ‘>= 0’ is always true [-Wtype-limits]
6 | if (count >= 0) {
| ~~~~~~^~~~The program prints both lines, because the first test is always true and the second test passes for 5. The warning is the only sign that the first test does nothing. For a range that starts at 0 on an unsigned value, check only the upper limit.
Practice
Exercise 1
Write a condition that is true when age is from 13 to 19, including both 13 and 19.
Show the answer
The condition is age >= 13 && age <= 19. Printed as a number, the result is 1 for age equal to 15 and 0 for age equal to 20.
Exercise 2
What does this print when x is 5, without boolalpha?
int x = 5;
cout << (0 <= x && x < 5) << "\n";Show the answer
It prints 0. The number 5 is not less than 5, so the second comparison is false. The upper limit 5 is not included in this range. The condition works like the range from 0 up to but not including 5.
Exercise 3
Rewrite if (5 <= temp <= 30) so that it works. Then say whether it accepts a temperature of 40.
Show the answer
The correct form is if (temp >= 5 && temp <= 30). A temperature of 40 is not accepted. The second comparison temp <= 30 is false, so the whole && is false.
Exercise 4
Write a function bool is_letter(char c) that returns true when c is a lowercase letter from a to z or an uppercase letter from A to Z. Test it with q, Q, 5 and _.
Show the answer
The output is:
q: true
Q: true
5: false
_: falseEach range is written as two comparisons, and the two ranges are joined with ||, which means either range is enough.
Exercise 5
What does this print for y equal to 50, and what is the fix?
int y = 50;
cout << (y >= 1 || y <= 10) << "\n";Show the answer
It prints 1, which means true. Every number is either at least 1 or at most 10, so the || is always true. The fix is y >= 1 && y <= 10, which prints 0 for 50.
Summary
- A chain such as
1 <= x <= 10compiles in C++, but it means(1 <= x) <= 10, and the result does not depend onx. - Write a range as two comparisons joined by
&&, for examplex >= 1 && x <= 10. - Use
>=and<=for "including" a limit, and>and<for "not including" it. - Use
||only for the outside of a range, and check that the two conditions are the opposite ones. - Compile with
-Wall, and treat the warnings aboutX<=Y<=Zas bugs to fix.