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.
Pinned: paper authors do read these threads. Technical discussion welcome; keep it civil.
it only took us until 2026 to formally specify what values a
floatcan represent. by C++35 we should have networking AND a definition of what a number is. maybe.networking is scheduled for when we finish specifying what a float is. so: soon.
great paper. any news on networking?
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() + 0is — direct quote — “UB by omission” or “UB by wording hole” even iffloatadheres 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.Wait,
infinity() + 0can actually be UB? I have been writing that in production code for a decade.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.
The negative zero definition in §6.2 is the part I keep turning over. The paper adds to [basic.fundamental]:
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.C23 makes the same call. §7.12.3.4 in the C23 draft:
sqrtshall not receive a negative argument, and -0.0 is not negative under C23’s definition either — which C++ inherits through<cmath>. Thesqrt(-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.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.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.Agreed. The definition is correct. The audit is future work. That’s what R2 is for.
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:
These are distinct instantiations. GCC and Clang both mangle
_Z3boxILf7fc00000EEand_Z3boxILf7fc00001EEas 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.
NaN boxing in template parameters. This is fine. Everything is fine. I love this language.
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.
meanwhile in Rust:
f32doesn’t implementEqbecause NaN != NaN. ThePartialEq/Eqsplit forces you to acknowledge that float comparison is partial. No paper needed, no committee, no wording debt. Just type system design from 2015.skill issue. write it in C.
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.”
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.
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.3So your
fpclassifycan return a value that isn’tFP_NORMAL,FP_SUBNORMAL,FP_ZERO,FP_INFINITE, orFP_NAN. It can returnFP_ROP. The C++ standard has silently permitted this since forever because it inheritsfpclassifyfrom 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
fpclassifycan 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.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.
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.
The
has_signaling_NaN/is_iec559contradiction fix in §6.5 is overdue. On WASM targets, GCC and Clang both reportis_iec559 = trueforfloatwhile simultaneously reportinghas_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.[removed by moderator]
what did they say
something about how -0.0 should obviously be negative and the committee has lost touch with reality. got the notification before it came down.
Want to master MODERN C++20/23/26 in just 30 days? My comprehensive course covers everything from lambdas to coroutines to floating-point best practices! Limited time offer — use code CPP2026 for 40% off! 🚀
not the thread for this. downvote and report.
godbolt.org. Free. Instant. Thousands of compilers. Essential when you’re chasing NaN bit patterns through symbol mangling.
Early bird registration ends May 15. The conference for the C++ community.