C++'s std::optional<bool> Truthiness Trap: The Guard That Fires Whether the Flag Is True or False

2026-07-21

This billing routine looks at a per-user override flag. If the user has explicitly opted out of billing (skip_billing = true), we skip the charge. Otherwise we process payment. The flag is stored in a map, so a missing entry means "not set — use the default (bill them)."

#include <optional>
#include <iostream>
#include <unordered_map>
#include <string>

std::unordered_map<std::string, bool> user_flags;

std::optional<bool> get_user_flag(int user_id, const std::string& flag) {
    auto key = std::to_string(user_id) + ":" + flag;
    auto it = user_flags.find(key);
    if (it == user_flags.end()) return std::nullopt;
    return it->second;
}

void process_payment(int, double);  // charges the card

void charge_user(int user_id, double amount) {
    auto skip_billing = get_user_flag(user_id, "skip_billing");
    if (skip_billing) {
        std::cout << "Skipping billing for user " << user_id << "\n";
        return;
    }
    process_payment(user_id, amount);
}

The unit tests pass. Users without any flag get billed. Users with skip_billing = true are skipped. Ship it.

Then support pings you: a customer who explicitly turned billing on (setting skip_billing = false through a UI toggle) hasn't been charged in three months.

The Bug

std::optional<T> has an explicit operator bool(). It returns true when the optional contains a value — not when that value is truthy. So if (skip_billing) asks "is the flag present?", not "is the flag set to true?"

For a std::optional<int> or std::optional<std::string> the type mismatch is obvious the moment you try to use it. But std::optional<bool> collapses two orthogonal pieces of information — present/absent and true/false — into one syntactic check, and the compiler will happily accept whichever one you meant, silently doing the other.

The three states you actually have are nullopt, bool(false), and bool(true). The buggy code treats them as two: absent vs. present. So the customer who set skip_billing = false lands in the "present" bucket and gets the skip path — the exact opposite of what they asked for.

The fix is to be explicit about which question you're asking:

if (skip_billing.value_or(false)) {   // default when absent: don't skip
    std::cout << "Skipping billing for user " << user_id << "\n";
    return;
}
process_payment(user_id, amount);

Or, if you want the intent to leap off the page:

if (skip_billing.has_value() && *skip_billing) { ... }

Both compile to the same thing. Both are impossible to misread six months later. if (opt) on an optional<bool> is almost never what you want — some linters (clang-tidy's bugprone-optional-value-conversion family, and a few in-house checkers) flag it precisely because it's a landmine.

A broader lesson: any type that overloads operator bool to mean "engaged / valid / non-null" becomes hazardous the moment you put a bool inside it. The same trap lurks in std::variant<std::monostate, bool>, in hand-rolled Result<bool> types, and — in other languages — in Rust's Option<bool> (though Rust forces you to match or call .unwrap_or(), which shuts the door). C++'s implicit-ish conversions leave the door wide open.

Key Takeaway: if (opt) asks whether an std::optional is engaged, not whether its contents are truthy — for optional<bool>, always spell out opt.value_or(default) or opt && *opt.

All newsletters