r/wg21
P3938R1 — Values of floating-point types WG21
Posted by u/fp_wording_archaeologist · 6 hr. ago

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

It is 2026 and the C++ standard still does not formally specify what values a floating-point type may represent. Can a float hold infinity from the core language’s perspective? Is negative zero negative? If you write float as a non-type template parameter and pass two NaN bit patterns with different payloads, do you get one instantiation or two? If you answered “obviously yes” to all of the above, Jan Schultke is here to point out the standard never said so.

P3938R1 is a pure clarification paper — no new semantics, no breaking changes. It documents what every major implementation already does and formalizes the answers to roughly fifteen questions that have been wording holes since C++98. Negative zero is not negative (it does not compare less than zero). Floating-point literals never have a negative sign bit, so 0.0 is positive by mandate. Two NaN values with different bit patterns are distinct template arguments, matching GCC, Clang, and MSVC. “Adhering to ISO/IEC 60559” gets an actual definition in [basic.fundamental]: value representation only, not operations. There is also a sentence about VAX FP_ROP that will make your Friday.

Wording is relative to N5032. Cross-reference CWG3129 and the companion paper P3899R1 on floating-point overflow if you want the full picture of where SG6 is taking the numerics wording.

▲ 78 points (91% upvoted) · 28 comments
sorted by: best
u/STL_Moderator 1 point 5 hr. agoModerator

Pinned: paper authors do read these threads. Technical discussion welcome; keep it civil.

u/fp_years_of_waiting 287 points 5 hr. ago

it only took us until 2026 to formally specify what values a float can represent. by C++35 we should have networking AND a definition of what a number is. maybe.

u/networking_in_2042 198 points 4 hr. 30 min. ago

networking is scheduled for when we finish specifying what a float is. so: soon.

u/networking_in_2042 341 points 4 hr. ago

great paper. any news on networking?

u/ub_wording_spelunker 43 points 4 hr. agoCompiler Dev

It is not specified what values a floating-point type may represent in C++

That opening line has been true since 1998. But §3.11 is where it gets spicy. The paper explicitly states that numeric_limits<float>::infinity() + 0 is — direct quote — “UB by omission” or “UB by wording hole” even if float adheres to ISO/IEC 60559. “Adheres to” has always meant value representation only, not that C++ arithmetic maps to IEC 60559 operations.

Every test suite that ever checked std::isinf(std::numeric_limits<float>::infinity() + 0.0f) was testing implementation mercy, not a standard guarantee. It works because GCC, Clang, and MSVC all do what IEC 60559 says without being required to.

The fix here is intentionally narrow: define what “adheres to” means, mandate the sign-flip behavior of unary - on zero, and explicitly leave C++ operation semantics to a future paper. Surgical. The right call given the wording debt.

u/just_a_cpp_dev_42 8 points 3 hr. 30 min. ago

Wait, infinity() + 0 can actually be UB? I have been writing that in production code for a decade.

u/ub_wording_spelunker 19 points 3 hr. agoCompiler Dev

It works because implementations do what IEC 60559 says even without being required to. The standard is catching up to reality. That is literally what this paper is — a formalization of existing implementation intent, not a behavior change.

u/negative_zero_pedant 31 points 3 hr. agoSafety / Correctness

The negative zero definition in §6.2 is the part I keep turning over. The paper adds to [basic.fundamental]:

A value is negative if and only if it compares less than 0.

Clean mathematically. But now consider std::sqrt(-0.0). The Preconditions in [cmath.syn] say the argument shall be non-negative. Under the new definition, -0.0 is non-negative and passes. Fine — sqrt(-0.0) returns -0.0 per IEC 60559 and that’s the right behavior. But those precondition clauses predate this formal definition and weren’t written with a formally-defined ‘negative’ in mind. The paper adds the definition in [basic.fundamental] without auditing every downstream <cmath> precondition that now acquires a new meaning. §4 explicitly calls it “low-hanging fruit” and punts. That’s probably the right call. But someone has to come back.

u/wg21_wording_follower 18 points 2 hr. 45 min. ago

C23 makes the same call. §7.12.3.4 in the C23 draft: sqrt shall not receive a negative argument, and -0.0 is not negative under C23’s definition either — which C++ inherits through <cmath>. The sqrt(-0.0) case is handled by implementation practice in both languages, not by precondition language. Changing the definition of ‘negative’ to include negative zero would require auditing every <cmath> function with a non-negative precondition, and would create a C/C++ divergence we can’t walk back. C23 compatibility is load-bearing here. The paper is correct to stay aligned.

u/negative_zero_pedant 22 points 2 hr. 20 min. agoSafety / Correctness

I understand the C23 alignment argument and I’m not contesting the definition itself. My concern is narrower: the definition lands in [basic.fundamental] and propagates to every use of ‘negative’ in the standard. But the existing Preconditions entries in [structure.requirements] that say ‘argument shall be non-negative’ weren’t written with a formally-defined ‘negative’ in mind. Post-paper, those preconditions acquire new, precise meaning. Fine! But was that the intended meaning of every such precondition? For most <cmath> functions the answer is yes. For some edge cases — especially domain-restriction functions where sign-of-zero changes the result — it’s not obvious. The paper changes the semantics of existing text without touching that text. That’s a wording interaction SG6 should explicitly bless in the review.

u/wg21_wording_follower 15 points 1 hr. 50 min. ago

That’s a fair point, actually. The definition in [basic.fundamental] does implicitly reinterpret existing Preconditions clauses without touching them. SG6 should cross-reference [structure.requirements] usage in the R2 or a follow-up paper — particularly for <cmath> functions where the sign of zero affects the result. The C23 parallel is still the right call, but the audit concern is real. I’ll note this when the paper comes up for review.

u/negative_zero_pedant 9 points 1 hr. 15 min. agoSafety / Correctness

Agreed. The definition is correct. The audit is future work. That’s what R2 is for.

u/template_ntt_enjoyer 38 points 3 hr. agoMetaprogramming

The §6.4 change is the quietest one with the most interesting implementation story. Before this paper, [temp.type] said values of floating-point type must be “identical” for template-argument-equivalence — P1907R1’s vague 2019 patch. Every compiler implemented it as “bitwise identical” since C++20, mangling the bit representation as an integer into the symbol name. This paper just makes that the standard requirement.

Consequence:

template <float V> struct box {};

// Two qNaN values: same IEEE 754 value, different bit patterns
using a = box<std::bit_cast<float>(0x7fc00000)>;
using b = box<std::bit_cast<float>(0x7fc00001)>;

static_assert(!std::is_same_v<a, b>); // passes: GCC, Clang, MSVC

These are distinct instantiations. GCC and Clang both mangle _Z3boxILf7fc00000EE and _Z3boxILf7fc00001EE as separate symbols. The new “bitwise identical” definition in [basic.fundamental] codifies this. Before P3938R1, the standard said “identical” and left you to guess whether that meant IEEE 754 value identity or bitwise identity. Now it’s explicit.

The implication for NaN-payload-encoded constants — think NaN boxing for type-tagged metadata in template contexts — is real. This is the right call.

Edit: 0x7fc00000 is the canonical quiet NaN for binary32, not a signaling NaN. The hex is correct.

u/constexpr_everything_2019 67 points 2 hr. 30 min. ago

NaN boxing in template parameters. This is fine. Everything is fine. I love this language.

u/ub_wording_spelunker 21 points 2 hr. agoCompiler Dev

The note is doing a lot of work in one sentence: “it is possible that two values are the same but not bitwise identical.” Making explicit that value identity ≠ bitwise identity is essential. A surprising number of people think IEEE 754 equality and object representation equality are the same concept. They are not, and C++ template argument equivalence was always about bits, not IEEE semantics. Now the standard says so.

u/rust_enjoyer_2024 19 points 4 hr. ago

meanwhile in Rust: f32 doesn’t implement Eq because NaN != NaN. The PartialEq/Eq split forces you to acknowledge that float comparison is partial. No paper needed, no committee, no wording debt. Just type system design from 2015.

u/c_purist_2046 156 points 3 hr. 30 min. ago

skill issue. write it in C.

u/template_ntt_enjoyer 14 points 3 hr. agoMetaprogramming

Rust’s const generics have the same NaN problem — you can’t use a float NaN as a const generic at all, the compiler rejects it. Which is one approach. C++ says “different bit patterns are different template arguments”, which is actually more useful for NaN-boxing patterns. Neither is wrong; they’re different tradeoffs between “ban the footgun” and “make the footgun load-bearing and document it carefully.”

u/rust_enjoyer_2024 8 points 2 hr. ago

fair. Rust bans NaN in const contexts entirely, which sidesteps canonicalization by refusing to engage. C++ makes every bit pattern meaningful and documents it. I respect the consistency even if it makes me nervous.

u/netbsd_archaeologist 47 points 3 hr. 30 min. ago

The paper buries the lead in §3.10: floating-point types may represent implementation-defined values beyond NaN, infinity, and finite numbers. The example is VAX FP_ROP — a “reserved operand” that traps when you try to process it, like integer divide-by-zero but for floats. NetBSD documents it: https://man.netbsd.org/NetBSD-10.1/fpclassify.3

So your fpclassify can return a value that isn’t FP_NORMAL, FP_SUBNORMAL, FP_ZERO, FP_INFINITE, or FP_NAN. It can return FP_ROP. The C++ standard has silently permitted this since forever because it inherits fpclassify from C’s <math.h>, which explicitly allows implementation-defined classifications. P3938R1 is the first C++ paper that actually writes this down.

If you write a floating-point classifier that assumes fpclassify can only return five categories, you have a latent bug on VAX targets. Which, granted, you probably don’t have. But the principle stands: the standard’s five-category model was never exhaustive. Now it says so explicitly.

u/definitely_not_a_mainframe_dev 23 points 2 hr. 30 min. ago

I work on systems adjacent to legacy numerical coprocessors with trap modes similar to VAX FP_ROP. This is not theoretical. The “five categories are exhaustive” assumption causes real bugs in portable FP classifier code. Nice to see the standard acknowledging the constraint exists.

u/process_cynic_embedded 89 points 4 hr. ago

committee gonna committee. seven years of C++20, three years of C++23, and we are now writing down things every implementer has known since the 80s. I respect it. the wording machine grinds slow but it grinds. at least it is not another paper proposing to make signed integer overflow defined behavior.

u/wasm_simd_survivor 26 points 2 hr. agoCompiler Dev

The has_signaling_NaN / is_iec559 contradiction fix in §6.5 is overdue. On WASM targets, GCC and Clang both report is_iec559 = true for float while simultaneously reporting has_signaling_NaN = true — even though every WASM f32 instruction canonicalizes NaNs, making sNaN indistinguishable from qNaN in practice.

I have wanted to fix this for two years and could not do it unilaterally without breaking conformance with is_iec559. The new note in [numeric.limits.members] permits implementations to treat all sNaN as qNaN “even for types that adhere to ISO/IEC 60559.” That’s the carve-out we needed. Finally official permission to not pretend WASM has signaling NaNs when it functionally does not.