r/wg21
P3938R1 - Values of floating-point types WG21
Posted by u/sg6_lurker · 8 hr. ago

Document: P3938R1
Author: Jan Schultke
Date: 2026-02-20
Audience: SG6, EWG, CWG

Jan Schultke takes on a surprisingly fundamental question: what values can a C++ floating-point type actually represent? Turns out the standard is vague on whether infinity and NaN exist from a core language perspective, whether negative zero is negative, and whether 0.0 is guaranteed to be positive zero.

The paper walks through about fifteen of these questions, finds answers in existing practice and scattered wording, and proposes core language changes to nail it all down. Notable finding: numeric_limits<float>::infinity() + 0 is technically UB under the current standard, because C++ never actually maps its expressions to ISO/IEC 60559 operations. Also clarifies template argument equivalence for floats - two NaN bit patterns with different payloads produce distinct template instantiations, and the paper proposes wording to match that.

Targets SG6, EWG, and CWG. Mostly encoding status quo into normative wording rather than proposing new behavior.

▲ 47 points (89% upvoted) · 8 comments
sorted by: best
u/definitely_not_a_NaN 34 points 5 hr. ago
This implies that e.g. numeric_limits<float>::infinity() + 0 is not infinity, but UB by omission or UB by wording hole

TIL that adding zero to infinity is technically undefined behavior in C++. I have been living a lie for fifteen years.

u/zero_divided_by_zero 71 points 4 hr. ago

Wait until you find out that dividing by zero is also UB. Yes, even for floats. The standard says "the behavior is undefined" in [expr.mul] with zero caveats for floating-point types. Every 1.0f / 0.0f you ever wrote was a prayer, not a program.

u/compiles_first_try 28 points 6 hr. ago

We are 40+ years into C++ and we still haven't formally specified what values a float can hold. Just incredible. This is the kind of paper that makes you realize the standard is held together with vibes and implementation consensus.

u/rust_rewrite_when 8 points 5 hr. ago

Rust just made floats not Eq and not Ord and called it a day. Sometimes the answer is "this is hard, so let's make the type system remind you it's hard." Meanwhile C++ is still trying to retroactively figure out if negative zero is negative.

u/ieee754_pedant 16 points 3 hr. ago

I read the whole thing. It's well-structured but the framing is slightly misleading. The abstract says:

The goal of this paper is to answer these questions, not by making any evolutionary changes to the language, but by investigating what the status quo is and turning that into wording.

But several of the proposed wording changes are actually normative, not just descriptive:

1. Mandating that floating-point literals have no negative sign bit ([lex.fcon]). No implementation does this differently, but it was never required before.

2. Requiring unary - to flip the sign bit of zero ([expr.unary.op]). Again, all compilers do this, but the standard didn't say they had to.

3. Defining "bitwise identical" and using it for template argument equivalence ([temp.type]). This replaces the vague "identical" with something that has a concrete operational definition via std::bit_cast.

None of these are controversial - they're all encoding what compilers already do. But "investigating what the status quo is and turning that into wording" undersells it. These are normative changes that narrow the implementation space. That's the right thing to do here. I just wish the paper owned it more directly instead of framing itself as purely descriptive.

The Q&A section (section 3) is worth reading on its own as a reference. I've had the "is negative zero negative" argument at work more than once and the answer ("no, because negative means less than zero, and -0.0 compares equal to 0.0") is one of those things that's obvious once someone writes it down.

u/template_nttp_fan 9 points 2 hr. ago

Section 3.14 on template argument equivalence is the part I care about most. The current wording says floating-point template arguments must be "identical" but never defines what that means for floats. All three compilers just mangle the bit pattern into the symbol name:

_Z1fILf7fc00000EEvv:   // default qNaN
        ret
_Z1fILf7fc00001EEvv:   // different qNaN payload
        ret

Two distinct instantiations for what ISO/IEC 60559 considers the same value. The paper proposes "bitwise identical" via std::bit_cast which matches what compilers already do and what P1714R1 originally intended before it got rejected in favor of P1907R1.

This also means std::constant_wrapper would work correctly with distinct NaN payloads, which is a nice side effect.

u/definitely_not_a_NaN 4 points 1 hr. ago

NaN payloads as template arguments. That is deeply cursed and I love it.

u/wasm_dev_42 5 points 47 minutes ago

The sNaN relaxation for is_iec559 is the right call. On WASM targets, f32/f64 instructions don't handle signaling NaNs at all - they silently get treated as quiet NaNs. But GCC and Clang still report is_iec559 == true because the format conforms to IEEE 754, just not the signaling behavior.

An implementation is permitted to treat all signaling NaNs as quiet NaN instead, even for types that adhere to ISO/IEC 60559.

This fixes a real contradiction that has bitten people writing portable numeric code. The alternative would be forcing is_iec559 = false on WASM, which would break a lot of if constexpr branches that people use to select IEEE-aware code paths. Worse for everyone.