Decomposing floating-point numbers
- Document number:
- P4382R0
- Date:
2026-10-10 - Audience:
- SG6
- Project:
- ISO/IEC 14882 Programming Languages — C++, ISO/IEC JTC1/SC22/WG21
- Reply-to:
- Jan Schultke <janschultke@gmail.com>
Matt Borland <matt@mattborland.com> - GitHub Issue:
- wg21.link/P4382/github
- Source:
- github.com/eisenwave/cpp-proposals/blob/main/src/float-decomposition.cow
Contents
Introduction
Refresher on floating-point model and representations
The problem
Motivation
Design
Handling of non-finite inputs
Why non-finite inputs should be checked for
Result of significand and exponent for non-finite inputs
Why not a <cmath> function?
Exponent guarantee for binary interchange floating-point
Decimal floating-point
Choice of header
Composing floating-point numbers
Implementation experience
Wording
[headers.cpp]
[version.syn]
[float.decompose]
[decompose.syn]
[float.decompose.class]
[float.decompose.properties]
[float.decompose.accessors]
[float.decompose.boolean]
[float.decompose.function]
References
Appendix — Sample implementations
decompose_float(float64_t)
frexp(float64_t, int*)
frexpq(float64_t, int*)
1. Introduction
The C++ standard library is currently lacking an elementary operation for decomposing floating-point numbers into integers. Such an operation is useful for a wide variety of applications, internally present in standard libraries already, and should be exposed to the user.
1.1. Refresher on floating-point model and representations
To understand what it means to decompose
a floating-point number,
one must first consider the mathematical model of floating-point numbers.
The C23 standard draft [N3220]
describes the model of floating-point numbers in
§5.2.5.3.3 as follows:
The following parameters are used to define the model for each floating type:
sign () base or radix of exponent representation (an integer ) exponent (an integer between a minimum and a maximum ) precision (the number of base- digits in the significand) nonnegative integers less than (the significand digits) For each floating type, the parameters , , , and are fixed constants.
For each floating type, a floating-point number () is defined by the following model:
Or grossly simplified, one could say:
This model is not just a theoretical basis, but closely matches how floating-point numbers are represented in memory; the ISO/IEC 60559 standard defines several binary interchange formats where a floating-point number is represented using
- a single sign bit (representing )
- exponent bits (representing ), and
- significand bits (representing ).
Thus, many operations are implemented by decomposing floating-point numbers into these components.
(which multiplies with
given ), one needs to
- decompose into its floating-point components
- adjust the exponent by , and
- recompose the floating-point number with the adjusted exponent.
with output in hex notation,
where , one needs to:
- decompose the input into its floating-point components,
- bit-shift and convert the significand (integer) to hexadecimal digits,
- add/remove trailing zeros and/or insert a decimal point, and
- print '
p ' followed by the (adjusted) exponent in decimal.
Of course, this is a gross simplification, and decomposing the floating-point number happens in decimal notation too, but there is a less direct relationship between the significand and the printed digits.
for floating-point numbers,
one cannot simply use the builtin operator
but needs to decompose floating-point numbers to ensure additional requirements such as
NaNs with negative sign bit comparing less than NaNs with positive sign bit.
While it is possible to implement such requirements
in terms of , , etc.,
some standard libraries decompose fully by hand
using and various bitwise operations.
See
1.2. The problem
Frustratingly, the decomposition described above is already implemented in standard libraries, just not exposed to the user. It is also difficult for the user to implement decompositions manually, especially with good performance, and especially in a way that portably supports exotic floating-point formats.
The standard library only offers much higher level operations such as
(which decomposes into a normalized fraction in ranges
and an exponent);
it does not provide the low-level building block that could be used to implement
, , , etc.
We therefore propose adding a function.
2. Motivation
The key benefit of a function
is that it allows the user to implement floating-point operations
without needing special knowledge about the layout of the floating-point type.
to a type
(i.e. an arbitrary-precision integer),
one would need to split the
into an integer and binary exponent , then
BigInt ( ) * pow ( , BigInt ( ) This approach can be found in various arbitrary-precision arithmetic libraries in practice.
An additional benefit is that it acts as a swiss-army knife
for asking many questions about a floating-point number at the same time efficiently.
- Checking if a number is zero requires checking if the significand is zero.
- Checking if it's finite requires checking if the exponent has a certain value (for binary interchange formats).
- The NaN payload becomes extractable as the significand itself.
- Subnormals can be converted to integers without first needing to be normalized.
- […]
See §3.2. Why not a
Another way to look at is that it performs a transition
from the floating-point domain into the integer domain,
which would be very difficult to implement correctly for the user by themselves.
This domain transition happens in the aforementioned construction,
and when printing floating-point numbers.
Note that is sufficient
for all formats in common use except .
3. Design
The proposed API is as follows:
A lot of design questions can be quickly answered in a few sentences:
- Why is
not simply an aggregate?float_decomposition - Because this gives the implementation more flexibility regarding the layout, including the use of bit-fields. Also, structured bindings would be undesirable due to potential bugs from getting the order of the members wrong.
- Why is it
?constexpr -
Most floating-point operations
(but especially inspection operations like
) are alreadystd :: isinf , so this one should be as well.constexpr - Why is it freestanding?
-
The operation is elementary and does not raise any floating-point exceptions,
nor does it interact with
. There is no technical reason it couldn't or shouldn't be freestanding.errno - What is
?significand_type -
It is an implementation-defined unsigned integer type.
This is needed because there are types such as
where the result would not fit into other types likestd :: float128_t .unsigned long long - Why does
returnexponent ( ) ?int -
Because
is sufficient for every real-world floating-point format, even if it has the minimum width of 16 bits.int has only 15 exponent bits. The return type can be changed in the future should a floating-point format ever emerge that requires more thanstd :: float128_t can provide.int exponents are also widespread in the standard library already, such as inint . Even if a format with more than 16 exponent bits emerged (such as an implementation of ISO/IEC 60559 256-bit binary interchange floating-point types), the platforms supporting suchint numeric_limits < float > :: max_exponent ultra-precise floating-point types
are unlikely to have 16-bit.int - Why is
provided as a static member?radix -
Of all the
properties,numeric_limits is the most important property in this context. It provides the minimum knowledge required forradix to be interpreted correctly. That justifies providing easy access to it.float_decomposition - What about all the other static constants?
-
These are helpful for metaprogramming since
andint may be much larger than the actual required width. If the user has a guarantee that the significand is only, say, 3 bits large (for FP8-E4M3), they might choose a different algorithm when subsequently processingsignificand_type , even ifstd :: float_decomposition issignificand_type . It is worth noting that these static constants provide information only about the integer results returned byunsigned int andexponent ( ) rather than being an extension ofsignificand ( ) .numeric_limits - What are
,std :: is_signaling , andstd :: is_subnormal ?std :: iszero - These are functions adopted from C23 via [P3935R2]. They are not available in C++26, but will almost certainly be available in C++29, likely before this paper is merged.
More involved design questions are addressed below.
3.1. Handling of non-finite inputs
3.1.1. Why non-finite inputs should be checked for
Firstly, it should be noted that determining whether a number is finite is typically extremely fast and would be a side product of the decomposition process. ISO/IEC 60559 binary interchange floating-point types are non-finite if and only if the exponent consists of all one-bits.
where needs to be extracted anyway as part of the decomposition,
and is a constant.
For , the added cost to the critical path is merely:
See also §
In the event that even this tiny check is too costly,
it can be eliminated via up front in theory,
although in practice, the assumption has no effect.
Another possible way to eliminate the check would be via a separate
function that has UB for non-finite inputs,
but this is currently not proposed.
Simply handling non-finite inputs as UB would be a safety hazard;
unlike in Rust, there is no way to disable certain checks in unsafe mode
,
so it's either always or never unsafe.
Infinities and NaNs can often arise unexpectedly at runtime,
so the potential for running into UB is great.
Treating NaNs as UB would also be unprecedented;
all of provides functions with wide contracts
that handle non-finite inputs gracefully,
and this is the ISO/IEC 60559 approach in general.
3.1.2. Result of significand and exponent for non-finite inputs
For ISO/IEC 60559 binary interchange types
the obvious
non-finite behavior would be if
-
returned the contents of the significand representation reinterpreted as an integer. This is useful for examining NaN payloads.significand ( ) -
returned the exponent adjusted using the same bias as for finite numbers. This means that the exponent of would be greater than that of any finite number, which makes comparisons ofexponent ( ) between results meaningful, unless NaN is involved.exponent ( )
This behavior is obvious
because it makes the original value
bitwise reconstructible from the decomposition, and
both infinities and NaN are handled identically using the same minimal logic
(see §
For ISO/IEC 60559 decimal interchange floating-point types,
infinity, qNaN, and sNaN are represented using the bit patterns
, , and respectively,
in the so-called combination field,
but this combination field is not simply the exponent.
This makes it unclear
what the results of and should be specifically,
and perhaps this issue would be better addressed when properly providing
support for decimal floating-point types in and the rest of the standard library.
The paper leaves this part implementation-defined.
For any other floating-point types,
the results are implementation-defined
because we don't know their representation down to the last bit anyway,
so we cannot say what a reasonable mapping of values to
and would be.
However, the implementation might still provide reasonable behavior
for types very similar to binary interchange types,
like x87 80-bit , , etc.
and should be recommended to do so.
3.2. Why not a <cmath> function?
Another design option would be to go through WG14 and
standardize a quantized function
similar to , like:
Unlike ,
which returns a normalized fraction,
this could return a number that is always a (small) integer.
The significand integer can then be obtained via ,
and that conversion would always be exact given an integer
with enough bits to represent the significand (except for non-finite inputs).
The key reason against this direction
is that does only a portion of the work.
constructor
as in the motivating example with in §2. Motivation
using the hypothetical
vs. the hypothetical :
The key observation is that does the work
of four functions simultaneously:
-
, by yielding the significand as an integer as well as the corresponding base-2 exponent;std :: frexpq -
, by identifying infinities and NaNs by their exponent;std :: isfinite -
, by splitting the sign bit from the significand; andstd :: fabs -
, by extracting the sign bit separately.std :: signbit
Miraculously,
is also a potentially cheaper operation than
despite being vastly more useful.
Specifically,
would be required to normalize subnormal numbers
rather than merely adjusting the exponent like for normal numbers.
By comparison,
can reinterpret the significand of both zeros and subnormal numbers directly.
In summary, is the better solution
for decomposing floating-point numbers because
- it provides a cleaner interface; and
-
it is a swiss-army knife that tends to solve several problems simultaneously,
like computing
,std :: frexpq , and various boolean tests simultaneously; andstd :: fabs -
it does not waste any work,
whereas
needs to test whether a number is subnormal or infinity but stores an unspecified value instd :: frexpq for such inputs (perhaps it could be defined not to waste this work, but* e stores an unspecified value, sostd :: frexp should presumably behave the same); andstd :: frexpq -
it is easy to implement a variety of operations on top of
, but not on top ofstd :: decompose_float ; for example,std :: frexpq does not seem like it should be involved instd :: frexpq , butstd :: signbit isstd :: signbit ( x ) ; andstd :: decompose_float ( x ) . sign_bit ( ) - it has cheaper handling of subnormals because it can reinterpret the significand directly, suggesting that it is a lower-level or more elementary operation; and
-
it is safer: since
requires the user to perform conversion to an integer type themselves, it presents a safety hazard if a user chooses an insufficient type and the conversion results in UB.std :: frexpq
3.3. Exponent guarantee for binary interchange floating-point
For binary interchange floating-point types
(, , possibly , etc.),
the proposed utility becomes significantly more useful if there is a strict guarantee
that the result of is the biased exponent
with a constant offset,
so we should provide that guarantee.
Several interesting properties emerge:
- Subnormal numbers and zeros can be distinguished from normal numbers based on the exponent alone.
-
Numbers with greater
result are always greater in magnitude, which is helpful for establishing orderings such asexponent ( ) .std :: strong_order - The underlying raw biased exponent can be recovered exactly.
-
The exponent bias and exponent width of the format
(which are not obtainable through
) become obtainable programmatically.numeric_limits -
There is only one possible choice for
, so all results become fully independent of implementation choices.significand ( )
These properties are too interesting and useful to forfeit
by leaving the choice of up to the implementation.
The guarantee also doesn't impose any additional runtime cost;
see §
3.4. Decimal floating-point
C23 optionally standardized
, , and ,
which are ISO/IEC 60559 decimal interchange floating-point types.
It is plausible
that a C++ implementation would be interested
in providing these as an extension for C compatibility, or
that we would want to standardize extended floating-point types like .
A key difference to binary floating-point types is that decimal floats have several different
representations of the same mathematical value.
For example, can be represented as
but also as
.
That or
is called the quantum
, and the exponent is called the quantum exponent
.
or ,
like for ,
where the format makes more sense if the number represents e.g. cents of a currency.
The C standard generally specifies not just the result
but also the quantum exponent of results of mathematical functions
because it is important information.
The design and wording of acknowledges this problem by
requiring that when reconstructing the original floating-point value,
it has the same quantum exponent, and that returns the quantum exponent
instead of some arbitrary exponent.
3.5. Choice of header
There should be a new header
providing just and
because no other header is a good fit:
-
contains a whole bunch of C mathematical functions, rather than low-level floating bit extraction. It is also not freestanding and has a connotation of C compatibility.<cmath> -
is way too heavy for this ultra-low-level freestanding utility. Fundamental floating-point utilities like<numeric> ,<limits> , or<stdfloat> traditionally have their own headers.<numbers> -
is focused on compile-time characteristics, not on runtime decomposition, and the name doesn't make sense for<limits> .std :: decompose_float -
provides only type aliases and is extremely lightweight, and it seems worth keeping it this way.<stdfloat>
3.6. Composing floating-point numbers
The paper provides no corresponding operation for composing floating-point numbers,
although a object can be explicitly converted
back to the original floating-point value.
A possible signature for this could be:
There are a number of reasons for why this is not provided:
-
There is less need for a composition operation
because functions such as
,std :: scalbn ,std :: setpayload , etc. already provide the necessary functionality. By contrast,std :: nan is insufficient for fully decomposing floating-point numbers.std :: frexp -
Composition often takes separate paths for different categories anyway.
For example,
handlesstd :: from_chars ," nan " , and" inf " inputs separately, so a" 0.0 " monolithic
composition function is not needed for those use cases. By comparison,is useful precise because it is monolithic; it decomposes the floating-point number, and boolean tests likestd :: decompose_float can be used to dispatch to various paths afterwards.is_finite ( ) -
ISO/IEC 60559 binary interchange formats encode NaNs
as simply having the greatest possible exponent.
This inevitably leads to two bad options:
-
Saying that certain
arguments makeexponent produce NaNs (i.e. making it astd :: compose_float monolithic
function) would makeawfullystd :: compose_float non-mathematical
. It would break the relation that the resulting magnitude is. Furthermore, if some large exponent value produces NaNs, what should produce? Supporting this form of NaN creation would result in a narrow contract that could otherwise be wide and could make the function a safety hazard. Furthermore, not every floating-point format represents NaNs in the same way, so saying that certainstd :: scalbn ( F ( significand ) , exponent ) arguments produce NaNs makes no sense for e.g. decimal floating-point types.exponent -
If this form of NaN creation is not supported,
is exactly equivalent tostd :: compose_float . That makes the function syntax sugar at best and unmotivated at worst.std :: copysign ( std :: scalbn ( F ( significand ) , exponent ) , sign ? - 1 : 1 )
-
Saying that certain
Additionally, the function
suffers from a hard design problem regarding the parameter type of :
-
If the significand type is templated as shown above,
different argument types such as
andunsigned int result in different template instantiations, but this seems unnecessary given that we arguably need just one type, namelyunsigned long .float_decomposition < F > :: significand_type -
Having a parameter of type
makes the function hard to use with wider integers. For example,float_decomposition < F > :: significand_type may have a significand type ofstd :: bfloat16_t (it is a 16-bit type after all). Thus, whilestd :: uint16_t is able to represent 32-bit integer values like ,std :: bfloat16_t would result in zero due to a lossy conversionstd :: compose_float ( false , 0 , uint32_t { 1 } << 20 ) . The function would be virtually unusable in generic code where we may not know the integer types and floating-point types.std :: uint32_t →std :: uint16_t -
Having a parameter of type
would solve the problem by moving the conversion to the call site and would be convenient due to deducingF , but would make the function even less useful; it would simply be sugar forF .std :: copysign ( std :: scalbn ( significand , exponent ) , sign ? - 1 : 1 )
4. Implementation experience
The proposed utility has been implemented many times over in every standard library and in various third-party libraries such as Boost.Charconv, beman.big_int, etc., although not with exactly the same interface as proposed here.
A sample implementation for
is provided in §
reference implementation
for every floating-point type is not feasible.
Similar to ,
only the implementation knows what set of floating-point types it supports
(some of which may be compiler extensions like CUDA
and not even considered extended floating-point types), and
only the implementation knows
how to identify a subnormal value efficiently for all of its supported formats.
5. Wording
The changes are relative to [N5054].
[headers.cpp]
In [tab:headers.cpp], add a new element to C++ library headers table:
[version.syn]
Add a feature-test macro to [version.syn] as follows:
[float.decompose]
Immediately following [numbers], insert a neighboring subclause:
Floating-point decomposition [float.decompose]
[decompose.syn]
Nested under [float.decompose], insert a new subclause:
Header <decompose> synopsis [float.decompose]
float_decomposition decompose_float [float.decompose.class]
Nested under [float.decompose],
insert a new subclause.
The marked lines are to be added only if
the corresponding functions have been adopted from C23 via [P3935R2].
Class template float_decomposition [float.decompose.class]
¶
objects
store information about individual components of
the value representation of an object of floating-point type .
This value representation is called the underlying value
of the object.
Copying and assigning such objects copies and assigns their underlying value,
respectively.
¶
A specialization of
exists for every cv-unqualified floating-point type ([basic.fundamental]).
Each such specialization is a standard-layout, trivially copyable class type.
¶
A program is ill-formed
if it defines a specialization of
or instantiates a specialization
where is not a cv-unqualified floating-point type.
¶
Throughout this subclause,
requirements specific to
ISO/IEC 60559 binary interchange floating-point types,
also apply to , if provided.
[float.decompose.properties]
Nested under [float.decompose.class], insert a new subclause:
Properties [float.decompose.properties]
¶
The lowest possible result of .
¶
The greatest possible result of .
¶
The minimum required width for a signed integer type to represent
both and .
¶
An implementation-defined unsigned integer type ([basic.fundamental])
capable of representing all possible results of .
A specialization of is ill-formed
if no such integer type exists.
¶
The greatest possible result of .
[Note:
This includes results of
where implicit bits have been concatenated to significand bits present
in the value representation of an underlying value.
If is an ISO/IEC 60559 binary interchange floating-point type,
equals .
— end note]
[float.decompose.accessors]
Nested under [float.decompose.class], insert a new subclause:
Accessors [float.decompose.accessors]
¶
Returns:
.
¶
Let be the underlying value.
Let be .
¶ Effects:
-
If is finite,
returns an integer andexponent ( ) returns an integer , such that . Which pair of and is chosen is a property of the implementation, except thatsignificand ( ) -
is chosen to be exactly representable in type
,T
[Example: IfisT and is , the greatest possible result ofstd :: float16_t satisfying this requirement issignificand ( ) , where65 ' 536 returnsexponent ( ) . — end example]- 16 -
if
is an ISO/IEC 60559 binary interchange floating-point type, , where is the biased exponent encoded in , and is the exponent bias constant for the format, as specified in ISO/IEC 60559.T
[Note: Consequently, the following statements are true:-
If and only if is zero or subnormal,
returnsexponent ( ) .min_exponent -
There is only one possible choice of
.significand ( ) -
If for two
objectsfloat_decomposition anda ,b isa . exponent ( ) > b . exponent ( ) and neither of their underlying values is NaN,true isvalue_type ( a ) > value_type ( b ) .true
-
If and only if is zero or subnormal,
-
if
is an ISO/IEC 60559 decimal interchange floating-point type, is the quantum exponent of .T
[Example: A floating-point value of an ISO/IEC 60559 decimal64 floating-point type can be decomposed so thatreturnssignificand ( ) and15 returnsexponent ( ) (corresponding to ), or so that- 1 returnssignificand ( ) and150 returnsexponent ( ) (corresponding to ), where- 2 equalsradix . The result of10 is equal to that of the C standard library functionexponent ( ) . — end example]llquantexpd64
subnormal flushing
, equals. — end note]copysign ( scalbn ( significand ( ) , exponent ( ) ) , T ( sign ( ) ) ) -
is chosen to be exactly representable in type
-
Otherwise, if
is an ISO/IEC 60559 binary interchange floating-point type,T returns a result one greater thanexponent ( ) returns for any finite , andexponent ( ) returns the significand bits in the value representation of , reinterpreted as an integer.significand ( )
[Note: If is negative or positive infinity,returns zero. If is NaN,significand ( ) returns the NaN payload. — end note]significand ( ) -
Otherwise, the result of
andexponent ( ) is derived from in an implementation-defined manner.significand ( )
¶
Remarks:
If two objects have the same underlying value,
their respective and results are equal.
¶
Recommended practice:
If is not an ISO/IEC 60559 binary interchange floating-point type
but has similar representation,
implementations should return the same results
as if were an ISO/IEC 60559 binary interchange floating-point type.
SEE ALSO: ISO/IEC 9899:2024 7.12.15.4
¶ Returns: The underlying value.
¶
Remarks:
Floating-point exceptions may be raised,
the conversion may be inexact,
and the NaN payload may not be preserved.
If is an ISO/IEC 60559 decimal interchange floating-point type,
the result has the same quantum exponent as the underlying value.
equals .
,
which is not an ISO/IEC 60559 binary interchange floating-point type,
but is very similar to such a format;
it provides infinities and NaNs,
but has an explicit integer significand bit.
[float.decompose.boolean]
Nested under [float.decompose.class],
insert a new subclause.
The marked lines are to be added only if
the corresponding functions have been adopted from C23 via [P3935R2].
Boolean accessors [float.decompose.boolean]
¶
Returns:
, where
is the corresponding function in
with the underscore in the name removed, and
is the underlying value.
[float.decompose.function]
Nested under [float.decompose.class], insert a new subclause:
Function decompose_float [float.decompose.function]
¶
Constraints:
is a cv-unqualified floating-point type.
¶
Returns:
A object whose underlying value is .
¶ Remarks: No floating-point exceptions are raised, and no inexact operation takes place.