By the end of this chapter you can
- Write conditions that short-circuit correctly
- Use switch without falling through by accident
- Explain why comparing floating-point numbers with == is a trap
A program that always does the same thing is a calculation. A program that chooses is where the work starts.
if, and the shape of a condition
#include <iostream>
int main() {
const int temperature = 12;
if (temperature < 0) {
std::cout << "freezing\n";
} else if (temperature < 15) {
std::cout << "cold\n";
} else {
std::cout << "warm\n";
}
}The condition must be something convertible to bool. Comparisons produce one
directly: <, <=, >, >=, ==, !=.
Braces are optional for a single statement and you should use them anyway:
#include <iostream>
int main() {
const bool ready = false;
// Only the first line is conditional. The second always runs.
if (ready)
std::cout << "starting\n";
std::cout << " this looks conditional and is not\n";
std::cout << "\nwith braces, the grouping is what it looks like\n";
if (ready) {
std::cout << "starting\n";
std::cout << "this really is conditional\n";
}
}The indentation lies in the first case. Braces cost two characters and remove a whole category of edit-time mistake.
Declaring inside the condition
C++17 lets you declare a variable in the if itself, scoping it to the branches:
#include <iostream>
#include <map>
#include <string>
int main() {
const std::map<std::string, int> ages{{"ada", 36}, {"alan", 41}};
if (auto it = ages.find("ada"); it != ages.end()) {
std::cout << it->first << " is " << it->second << '\n';
}
// `it` does not exist here, which is exactly right.
if (auto it = ages.find("nobody"); it != ages.end()) {
std::cout << "found\n";
} else {
std::cout << "no entry for nobody\n";
}
}Use it whenever the variable is only meaningful inside the branch. A name that cannot leak cannot be misused later.
Boolean logic, and short-circuiting
&& is and, || is or, ! is not. The important property is that && and
|| short-circuit: they evaluate the right side only if they must.
#include <iostream>
#include <vector>
bool noisy(const char* label, bool value) {
std::cout << " evaluating " << label << '\n';
return value;
}
int main() {
std::cout << "false && ...\n";
if (noisy("left", false) && noisy("right", true)) {}
std::cout << "true || ...\n";
if (noisy("left", true) || noisy("right", true)) {}
std::cout << "\nshort-circuiting is what makes this safe:\n";
std::vector<int> v;
if (!v.empty() && v[0] > 0) { // v[0] never runs on an empty vector
std::cout << "first is positive\n";
} else {
std::cout << "empty, and we never touched v[0]\n";
}
}That last pattern — check that something exists, then look at it — depends entirely on short-circuiting. Reverse the operands and the program reads out of bounds before discovering there was nothing there.
Comparing floating-point numbers
Chapter 1.2 showed that 0.1 + 0.2 != 0.3. That has a direct consequence for
conditions:
#include <cmath>
#include <iostream>
int main() {
const double sum = 0.1 + 0.2;
if (sum == 0.3) {
std::cout << "equal\n";
} else {
std::cout << "not equal — and this is the branch that runs\n";
}
// Compare with a tolerance appropriate to the problem.
if (std::fabs(sum - 0.3) < 1e-9) {
std::cout << "close enough, which is the question you meant to ask\n";
}
}< and > on doubles are fine. It is == and != that are almost always the
wrong question — and != is worse, because a loop written
while (x != target) may never terminate.
switch
When you are comparing one value against many constants, switch says so more
clearly than a chain of else if:
#include <iostream>
#include <string_view>
enum class Direction { north, east, south, west };
std::string_view describe(Direction d) {
switch (d) {
case Direction::north: return "up";
case Direction::east: return "right";
case Direction::south: return "down";
case Direction::west: return "left";
}
return "unknown";
}
int main() {
for (Direction d : {Direction::north, Direction::east,
Direction::south, Direction::west}) {
std::cout << describe(d) << ' ';
}
std::cout << '\n';
}Switching over an enum class and handling every enumerator has a real benefit:
add a fifth direction and the compiler warns that the switch no longer covers
every case. A chain of else if with a final else would silently take the
fallback instead.
Fallthrough
A case without break continues into the next one. Sometimes that is what you
want; usually it is a bug:
#include <iostream>
void classify(int score) {
std::cout << score << ": ";
switch (score) {
case 0:
std::cout << "none ";
[[fallthrough]]; // deliberate, and says so
case 1:
case 2:
std::cout << "low\n";
break;
case 3:
std::cout << "medium\n";
break;
default:
std::cout << "high\n";
break;
}
}
int main() {
for (int score : {0, 1, 2, 3, 9}) classify(score);
}Two things there. Stacked labels (case 1: case 2:) share a body and are not
fallthrough — that is the normal way to group values. And [[fallthrough]];
marks a deliberate fall from one body into the next; without it, compilers with
-Wimplicit-fallthrough warn, which is exactly the warning you want.
int main() {
int value = 1;
switch (value) {
case 1:
int result = 10; // error: jump to case label crosses this
return result;
case 2:
return 0;
}
}The conditional operator
For choosing between two values rather than two actions, ?: is compact and
clear:
#include <iostream>
#include <string>
int main() {
const int count = 1;
// The conditional operator is an expression, so it can initialise a const.
const std::string word = count == 1 ? "item" : "items";
std::cout << count << ' ' << word << '\n';
const int a = 7, b = 3;
std::cout << "larger: " << (a > b ? a : b) << '\n';
}It earns its place when both branches produce a value of the same type and the
whole thing fits on one line. Nested conditionals — a ? b : c ? d : e — are
where it stops being clearer than an if.