The complement of true is true, except when it's false
The Paradox of the Boolean Complement: When ~true is true (and sometimes not)
By Matthew Taylor | October 1, 2026
Introduction
I recently spent some time analyzing P4313R1, a proposal for the C++26 standard. This paper suggests introducing a set of bitmask operations specifically for enumerations, which developers can opt into using a new annotation: [[=std::bitmask_type]].
To illustrate, consider the following implementation:
| Enum Member | Value | Description |
|---|---|---|
None | 0 | No permissions |
Read | 1 << 0 | Read access |
Write | 1 << 1 | Write access |
Execute | 1 << 2 | Execute access |
enum class [[=std::bitmask_type]] Permission {
None = 0,
Read = 1 << 0,
Write = 1 << 1,
Execute = 1 << 2,
};
The goal of [[=std::bitmask_type]] is to automatically provide the Permission type with bitwise operators, sparing the developer from manually overloading them. This exploration led me down a rabbit hole into the "murky underbelly" of how C++ handles integer operations.
The Mechanics of Integral Promotion
C++ compilers treat bitwise operations as built-in functions for any integral type. This includes types we don't always associate with integers, such as:
wchar_t- UTF character types (
char8_tthroughchar32_t) bool
Crucially, when you perform an operation like a | b on types smaller than an int, the compiler does not operate on the raw bits of those types. Instead, they undergo integral promotion.
Example of Promotion
Consider two short variables:
constexpr short perm_A { 1 << 0 };
constexpr short perm_B { 1 << 1 };
// The result of the bitwise OR is actually an int, not a short
static_assert(std::same_as<decltype(perm_A | perm_B), int>);
Generally, this is benign. If you cast the result back to a short, the extra bytes are truncated, leaving you with the expected value. To avoid "silent narrowing conversions," it is a good practice to use static_cast to return the result to the original type. However, integral promotion is mandatory; the developer cannot opt out.
The Hazard of Boolean Conversion
The real danger arises when combining promotion with boolean conversion. Unlike truncation, boolean conversion follows a strict logic:
When a non-zero integer is converted to bool, the original bit pattern is discarded and replaced with the value 1 (representing true).
Assembly Insight
If we take int x{ 10 }; bool b{ static_cast<bool>(x) };, the unoptimized x86-64 GCC 16.2 assembly looks like this:
mov DWORD PTR [rbp-4], 10Store 10
cmp DWORD PTR [rbp-4], 0Compare to 0
setne alSetALto 1 if not equal (non-zero)
mov BYTE PTR [rbp-5], alStore result inbool
As per the standard ([conv.bool]), the result depends solely on whether the value is zero or non-zero.
Putting it Together: The ~true Paradox
What happens when we evaluate ~true and cast it back to bool?
-
trueis promoted toint(value:1). - The bitwise NOT (
~) is applied. In C++20, two's complement is required, so~1becomes-2. - The value
-2is cast back tobool. - Since , it undergoes boolean conversion and becomes
true.
Therefore: static_cast<bool>(~true) == true. The complement of true is true.
Application to Enums
Since enums can use bool as an underlying type, we can create a "boolean enum":
enum class [[=std::bitmask_type]] boolean : bool { FALSE, TRUE };
The proposed operator~ in P4313R1 is defined as:
template <bitmask-like T>
constexpr T operator~(T lhs) noexcept {
return static_cast<T>(~to_underlying(lhs));
}
Following the logic: TRUE bool int ~ int boolean.
static_assert(~boolean::TRUE == boolean::TRUE);
When it's False
There is a catch. In some environments, like GCC, the result is different:
static_assert(~boolean::TRUE == boolean::TRUE);static_assert(~boolean::TRUE == boolean::FALSE);
To understand why, let's compare two functions in assembly:
enum class boolean : bool { FALSE, TRUE };
void f(int x, bool out) { out = static_cast<bool>(x); }
void g(int x, boolean out) { out = static_cast<boolean>(x); }
Assembly for f(int, bool):
cmp DWORD PTR [rbp-4], 0
setne dl ; Standard boolean conversion
Assembly for g(int, boolean):
mov eax, DWORD PTR [rbp-4]
and eax, 1 ; Masking the low bit!
mov BYTE PTR [rdx], al
The difference is stark. While f uses setne (any non-zero is true), g uses and eax, 1. This effectively truncates the integer to its least significant bit. Since -2 in binary ends in 0 (...11111110), the and 1 operation results in 0, which is FALSE.
Conclusion: When the compiler treats the cast as a direct assignment to the underlying bool type of an enum rather than a standard boolean conversion, the complement of true finally becomes false.