C++ / CONTROL FLOW
if statements and boolean conversions
Write if statements in C++ and predict exactly which values count as true, including integers, pointers, floats and types with explicit operator bool.
What you will learn
- Decide if a scalar is truthy by asking only whether it compares equal to zero
- Use if (init; condition) to scope a value to the branch that needs it
- Explain why explicit operator bool still works inside an if condition
- Spot if (x = 0) assignment bugs and the -Wparentheses warning that catches them
Understanding if statements and boolean conversions
An if statement takes a parenthesised condition and exactly one statement; braces make that statement a compound statement, and without them only the next single statement belongs to the if. The condition does not have to be a bool. It can be any expression whose type converts to bool, which covers int, char, double, raw pointers, and class types with a conversion operator, so if (n) and if (static_cast<bool>(n)) generate the same test. Since C++17 the parentheses may hold two parts separated by a semicolon, an initialiser followed by the condition, and anything declared there exists only for the duration of the if.
For scalars the conversion rule fits in one sentence: a zero value, a null pointer, or a null pointer-to-member becomes false, and every other value becomes true. That is why if (-1) is taken; the question is 'not zero', not 'positive'. The same rule covers floating point, where 0.25 is true and 0.0 is false, and it is the reason an empty C string still passes if (s) since the pointer itself is not null. In the other direction a bool converts to exactly 1 or 0, which starts to matter the moment a bool meets an arithmetic or comparison operator.
For class types the mechanism has a name, contextual conversion to bool, and an if condition is one of the places it happens, alongside !, &&, ||, and loop and conditional-operator conditions. Contextual conversion is permitted to call a conversion operator marked explicit, which is exactly why std::optional, std::unique_ptr and an input stream can be written as if (x) yet reject bool b = x;. Giving your own handle types an explicit operator bool buys that behaviour while keeping them out of arithmetic, where an implicit version would silently let handle + 1 or handle == other_handle compile.
<iostream>
int main() {
std::cout << std::boolalpha;
int count = 0;
double ratio = 0.25;
const char* name = nullptr;
if (count) {
std::cout << "count is truthy\n";
} else {
std::cout << "count is falsy\n";
}
if (ratio) {
std::cout << "ratio is truthy\n";
}
if (name) {
std::cout << "name points somewhere\n";
} else {
std::cout << "name is null\n";
}
std::cout << "bool(count) = " << static_cast<bool>(count) << '\n';
std::cout << "bool(-1) = " << static_cast<bool>(-1) << '\n';
std::cout << "bool(0.0) = " << static_cast<bool>(0.0) << '\n';
std::cout << "int(true) = " << static_cast<int>(true) << '\n';
}An if condition is not a bool you supply but an expression contextually converted to one, and for every scalar that conversion means 'does not compare equal to zero'.
Worked examples
Condition with an initialiser
Shows the C++17 two-part condition and why an index must be compared rather than tested for truthiness.
<iostream>
<string>
int main() {
std::string text = "if,statements";
if (auto comma = text.find(','); comma != std::string::npos) {
std::cout << "comma at index " << comma << '\n';
std::cout << "before it: " << text.substr(0, comma) << '\n';
} else {
std::cout << "no comma in " << text << '\n';
}
// comma no longer exists here
}Example explained
Line 1The initialiser part runs first, so find is called exactly once regardless of which branch is taken.
Line 2comma != std::string::npos must be spelled out: a match at index 0 converts to false, so if (comma) would report failure for any text starting with a comma.
Line 3comma is visible in both branches and dies at the closing brace, so the name cannot be reused by accident later in main.
explicit operator bool in a condition
Demonstrates that an if condition uses contextual conversion, which can call an explicit conversion operator.
<iostream>
struct Handle {
int fd;
explicit operator bool() const { return fd >= 0; }
};
int main() {
Handle active{3};
Handle broken{-1};
if (active) {
std::cout << "active is usable\n";
}
if (!broken) {
std::cout << "broken is not usable\n";
}
// bool b = broken; // error: conversion is explicit
bool b = static_cast<bool>(broken);
std::cout << std::boolalpha << "explicit cast: " << b << '\n';
}Example explained
Line 1explicit keeps the operator out of ordinary implicit conversions, so the commented-out bool b = broken; would not compile.
Line 2An if condition performs a contextual conversion to bool, which is allowed to call the explicit operator, so if (active) needs no cast.
Line 3operator! contextually converts its operand too, which is why !broken compiles and prints.
Line 4Outside those contexts you ask for the value with static_cast<bool>, and boolalpha prints it as false instead of 0.
Assignment in a condition
Shows what = instead of == does to both the branch taken and the variable.
<iostream>
int main() {
int retries = 3;
if (retries = 0) {
std::cout << "A: branch taken\n";
} else {
std::cout << "A: not taken, retries = " << retries << '\n';
}
retries = 3;
if (retries == 0) {
std::cout << "B: branch taken\n";
} else {
std::cout << "B: not taken, retries = " << retries << '\n';
}
}Example explained
Line 1retries = 0 is an assignment expression whose value is the value stored, so the condition converts 0 to false.
Line 2The first else prints retries = 0, proving the variable was clobbered before the test was even evaluated.
Line 3The second if uses ==, which already produces a bool and leaves retries at 3.
Line 4GCC and Clang report the first form under -Wparentheses; writing if ((retries = 0)) is the accepted way to state that the assignment was deliberate.
Important notes
Every non-zero double converts to true, including NaN, while 0.0 and -0.0 are both zero values and convert to false, so if (x) tells you only that x != 0.
A scoped enum has no implicit conversion at all, so if (state) fails to compile for an enum class; compare it against an enumerator instead.
Common mistakes
Typing if (x = 5) for if (x == 5): the condition is the assigned value 5, so the branch always runs and x has been silently overwritten.
Writing if (0 < x < 10): the first comparison yields a bool that converts to 0 or 1, both of which are less than 10, so the condition is always true.
Comparing an int with true, as in if (flags == true): true is promoted to 1, so flags of 2 fails the test even though if (flags) would succeed.
Try it yourself
Change, predict, then run
Declare const char* empty = ""; and const char* none = nullptr;, print static_cast<bool> of each with std::boolalpha, then add one if that prints "has characters" only when the pointer is non-null and its first character is not '\0'.
Open the C++ workspaceCheck your understanding
With int flags = 2;, what does this print: if (flags == true) std::cout << "A"; if (flags) std::cout << "B";
- A then B
- B only
- A only
- Nothing is printed
Show answer
In flags == true the usual arithmetic conversions promote the bool to int 1, so the test is 2 == 1 and fails. The second condition converts flags to bool instead, and any non-zero value becomes true, so only B prints. 'A then B' assumes == true means 'is truthy', which C++ never does; the conversion goes toward int, not toward bool.