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

There should be a standard-library function for decomposing floating-point numbers into a sign bit, exponent, and integer significand.

Contents

1

Introduction

1.1

Refresher on floating-point model and representations

1.2

The problem

2

Motivation

3

Design

3.1

Handling of non-finite inputs

3.1.1

Why non-finite inputs should be checked for

3.1.2

Result of significand and exponent for non-finite inputs

3.2

Why not a <cmath> function?

3.3

Exponent guarantee for binary interchange floating-point

3.4

Decimal floating-point

3.5

Choice of header

3.6

Composing floating-point numbers

4

Implementation experience

5

Wording

5.1

[headers.cpp]

5.2

[version.syn]

5.3

[float.decompose]

5.4

[decompose.syn]

5.5

[float.decompose.class]

5.6

[float.decompose.properties]

5.7

[float.decompose.accessors]

5.8

[float.decompose.boolean]

5.9

[float.decompose.function]

6

References

7

Appendix — Sample implementations

7.1

decompose_float(float64_t)

7.2

frexp(float64_t, int*)

7.3

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:

s sign (±1)
b base or radix of exponent representation (an integer >1)
e exponent (an integer between a minimum emin and a maximum emax)
p precision (the number of base-b digits in the significand)
fk nonnegative integers less than b (the significand digits)

For each floating type, the parameters b, p, emin, and emax are fixed constants.

For each floating type, a floating-point number (x) is defined by the following model:

x = s be ∑ k=1 p fk b−k , emin ≤ e ≤ emax

Or grossly simplified, one could say:

x = s be q , q ∈ ℕ

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

Thus, many operations are implemented by decomposing floating-point numbers into these components.

To implement std::scalbn(double x, int n) (which multiplies x with bn given n), one needs to

  1. decompose x into its floating-point components
  2. adjust the exponent by n, and
  3. recompose the floating-point number with the adjusted exponent.

To implement std::to_chars with output in hex notation, where b=2, one needs to:

  1. decompose the input into its floating-point components,
  2. bit-shift and convert the significand (integer) to hexadecimal digits,
  3. add/remove trailing zeros and/or insert a decimal point, and
  4. 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.

To implement std::strong_order::operator() 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 std::signbit, std::isnan, etc., some standard libraries decompose fully by hand using std::bit_cast and various bitwise operations. See microsoft/STL/.../stl/inc/compare and gcc-mirror/gcc/.../libsupc++/compare.

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 std::frexp (which decomposes into a normalized fraction in ranges ±[0.5,1) and an exponent); it does not provide the low-level building block that could be used to implement std::frexp, std::scalbn, std::to_chars, etc.

We therefore propose adding a std::decompose_float function.

2. Motivation

The key benefit of a std::decompose_float function is that it allows the user to implement floating-point operations without needing special knowledge about the layout of the floating-point type.

When implementing a conversion from a float to a BigInt type (i.e. an arbitrary-precision integer), one would need to split the float into an integer q and binary exponent e, then

return BigInt(q) * pow(b, BigInt(q)); // << e for binary floats

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.

See §3.2. Why not a <cmath> function? for a more detailed example.

Another way to look at std::decompose_float 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 BigInt construction, and when printing floating-point numbers.

When printing floating-point numbers, regardless of the type, a transition into the integer domain makes it possible to use the same underlying routine for all types with the same radix:

void print_float( // universal runtime library function std::uint64_t significand, int exponent, bool sign, int precision ); void print_float(float x, int precision) { auto dx = std::decompose_float(x); print_float(dx.significand(), dx.exponent(), dx.sign_bit(), precision); } void print_float(double x, int precision) { auto dx = std::decompose_float(x); print_float(dx.significand(), dx.exponent(), dx.sign_bit(), precision); } // support for various other types ...

Note that std::uint64_t is sufficient for all formats in common use except std::float128_t.

3. Design

The proposed API is as follows:

template<class T> struct float_decomposition { // properties using value_type = T; using significand_type = see below; // radix for the result of exponent() static constexpr int radix = numeric_limits<T>::radix; // lowest possible result of exponent() static constexpr int min_exponent = implementation-defined; // greatest possible result of exponent() static constexpr int max_exponent = implementation-defined; // signed integer width required to represent any exponent static constexpr int exponent_width = implementation-defined; // greatest possible result of significand() static constexpr significand_type max_significand = implementation-defined; // bit width of greatest possible significand() result, // which may be lower than the width of significand_type static constexpr int significand_width = bit_width(max_significand); // constructors constexpr float_decomposition() : float_decomposition(decompose_float(T{})) {} // accessors constexpr int sign() const noexcept; // returns -1 or 1 constexpr int exponent() const noexcept; // returns base-radix exponent constexpr significand_type significand() const noexcept; constexpr explicit operator value_type() const noexcept; // boolean properties constexpr bool is_finite() const noexcept; // std::isfinite(x) constexpr bool is_inf() const noexcept; // std::isinf(x) constexpr bool is_nan() const noexcept; // std::isnan(x) constexpr bool is_normal() const noexcept; // std::isnormal(x) constexpr bool sign_bit() const noexcept; // std::signbit(x) constexpr bool is_signaling() const noexcept; // std::issignaling(x) constexpr bool is_subnormal() const noexcept; // std::issubnormal(x) constexpr bool is_zero() const noexcept; // std::iszero(x) }; template<class T> constexpr float_decomposition<T> decompose_float(const T& x) noexcept; // freestanding

A lot of design questions can be quickly answered in a few sentences:

Why is float_decomposition not simply an aggregate?
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 std::isinf) are already constexpr, so this one should be as well.
Why is it freestanding?
The operation is elementary and does not raise any floating-point exceptions, nor does it interact with errno. There is no technical reason it couldn't or shouldn't be freestanding.
What is significand_type?
It is an implementation-defined unsigned integer type. This is needed because there are types such as std::float128_t where the result would not fit into other types like unsigned long long.
Why does exponent() return int?
Because int is sufficient for every real-world floating-point format, even if it has the minimum width of 16 bits. std::float128_t has only 15 exponent bits. The return type can be changed in the future should a floating-point format ever emerge that requires more than int can provide. int exponents are also widespread in the standard library already, such as in int numeric_limits<float>::max_exponent. 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 such ultra-precise floating-point types are unlikely to have 16-bit int.
Why is radix provided as a static member?
Of all the numeric_limits properties, radix is the most important property in this context. It provides the minimum knowledge required for float_decomposition to be interpreted correctly. That justifies providing easy access to it.
What about all the other static constants?
These are helpful for metaprogramming since int and significand_type 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 processing std::float_decomposition, even if significand_type is unsigned int. It is worth noting that these static constants provide information only about the integer results returned by exponent() and significand() rather than being an extension of numeric_limits.
What are std::is_signaling, std::is_subnormal, and 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.

The finiteness check for any binary interchange floating-point number boils down to

if (raw_exponent == max_raw_exponent) [[unlikely]]

where raw_exponent needs to be extracted anyway as part of the decomposition, and max_raw_exponent is a constant. For std::float64_t, the added cost to the critical path is merely:

cmp edx, 2047 ; if (edx == 20247) je .skip_adding_integer_bit ; goto skip_adding_integer_bit;

See also § decompose_float(float64_t).

In the event that even this tiny check is too costly, it can be eliminated via [[assume(std::isfinite(x))]] up front in theory, although in practice, the assumption has no effect. Another possible way to eliminate the check would be via a separate std::decompose_finite_float 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 <cmath> 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

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 § decompose_float(float64_t)).

For ISO/IEC 60559 decimal interchange floating-point types, infinity, qNaN, and sNaN are represented using the bit patterns 11110, 11111'0, and 11111'1 respectively, in the so-called combination field, but this combination field is not simply the exponent. This makes it unclear what the results of exponent() and significand() should be specifically, and perhaps this issue would be better addressed when properly providing support for decimal floating-point types in <cmath> 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 significand() and exponent() would be. However, the implementation might still provide reasonable behavior for types very similar to binary interchange types, like x87 80-bit long double, std::bfloat16_t, 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 frexp, like:

double frexpq(double x, int* exp) { // sample implementation assuming that numeric_limits<double>::radix == 2 double d = std::frexp(x, &exp); exp -= std::numeric_limits<double>::digits; return std::scalbn(d, std::numeric_limits<double>::digits); }

This is a naive implementation. See § frexpq(float64_t, int*) for a more plausible one.

Unlike frexp, which returns a normalized fraction, this could return a number that is always a (small) integer. The significand integer can then be obtained via static_cast, 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 frexpq does only a portion of the work.

Consider how to actually implement a BigInt constructor as in the motivating example with BigInt in §2. Motivation using the hypothetical std::frexpq vs. the hypothetical std::decompose_float:

struct BigInt { // typical BigInt implementations use a sign-and-magnitude representation bool sign; unsigned long* limbs; BigInt(std::unsigned_integral auto value, bool sign) : sign(sign), limbs(do_alloc_limb_array(value)) {} #ifdef USE_FREXPQ BigInt(std::float64_t x) : BigInt(0, false) { if (!std::isfinite(x)) { throw std::invalid_argument("non-finite input"); } int e; double xi = std::frexpq(x, &e); *this = BigInt(static_cast<std::uint64_t>(std::fabs(xi)), std::signbit(xi)); *this <<= e; } #else BigInt(std::float64_t x) : BigInt(0, false) { auto d = std::decompose_float(x); if (!d.is_finite()) { throw std::invalid_argument("non-finite input"); } *this = BigInt(d.significand(), d.sign_bit()); *this <<= d.exponent(); } #endif BigInt& operator<<=(int s) { if (s > 0) do_shift_left(s); if (s < 0) do_shift_right(-s); return *this; } // ... };

The key observation is that std::decompose_float does the work of four functions simultaneously:

Miraculously, std::decompose_float is also a potentially cheaper operation than std::frexpq despite being vastly more useful. Specifically, std::frexpq would be required to normalize subnormal numbers rather than merely adjusting the exponent like for normal numbers. By comparison, std::decompose_float can reinterpret the significand of both zeros and subnormal numbers directly.

In summary, std::decompose_float is the better solution for decomposing floating-point numbers because

3.3. Exponent guarantee for binary interchange floating-point

For binary interchange floating-point types (std::float32_t, std::float64_t, possibly float, etc.), the proposed utility becomes significantly more useful if there is a strict guarantee that the result of exponent() is the biased exponent E with a constant offset, so we should provide that guarantee. Several interesting properties emerge:

These properties are too interesting and useful to forfeit by leaving the choice of exponent() up to the implementation. The guarantee also doesn't impose any additional runtime cost; see § decompose_float(float64_t).

3.4. Decimal floating-point

C23 optionally standardized _Decimal32, _Decimal64, and _Decimal128, 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 std::decimal64_t.

A key difference to binary floating-point types is that decimal floats have several different representations of the same mathematical value. For example, 1.5 can be represented as 15×10-1 but also as 150×10-2. That 10-1 or 10-2 is called the quantum, and the exponent is called the quantum exponent.

The choice of quantum exponent is not just arbitrary but can control whether 1.5 gets printed as "1.5" or "1.50", like for java.math.BigDecimal.toString(), where the "1.50" 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 std::decompose_float acknowledges this problem by requiring that when reconstructing the original floating-point value, it has the same quantum exponent, and that exponent() returns the quantum exponent instead of some arbitrary exponent.

3.5. Choice of header

There should be a new header <decompose> providing just std::decompose_float and std::float_decomposition because no other header is a good fit:

3.6. Composing floating-point numbers

The paper provides no corresponding operation for composing floating-point numbers, although a std::float_decomposition object can be explicitly converted back to the original floating-point value. A possible signature for this could be:

template<class F, std::unsigned_integral Uint> constexpr F compose_float(bool sign, int exponent, Uint significand);

There are a number of reasons for why this is not provided:

Additionally, the std::compose_float function suffers from a hard design problem regarding the parameter type of significand:

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 std::float64_t is provided in § decompose_float(float64_t).

Providing a true third-party reference implementation for every floating-point type is not feasible. Similar to std::issubnormal, only the implementation knows what set of floating-point types it supports (some of which may be compiler extensions like CUDA __nv_fp8_e4m3 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:

<decompose>

[version.syn]

Add a feature-test macro to [version.syn] as follows:

#define __cpp_lib_decompose_float 20XXXXL // freestanding, also in <header>

[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]

// all freestanding namespace std { // [float.decompose.class], class template float_decomposition template<class T> struct float_decomposition; // [float.decompose.function], function template decompose_float template<class T> constexpr float_decomposition<T> decompose_float(const T& x) noexcept; }

[float.decompose.class]

Nested under [float.decompose], insert a new subclause. The marked lines are to be added only if the corresponding <cmath> functions have been adopted from C23 via [P3935R2].

Class template float_decomposition [float.decompose.class]

template<class T> struct float_decomposition { // [float.decompose.properties], properties using value_type = T; using significand_type = see below; static constexpr int radix = numeric_limits<T>::radix; static constexpr int min_exponent; static constexpr int max_exponent; static constexpr int exponent_width; static constexpr significand_type max_significand; static constexpr int significand_width = bit_width(max_significand); // constructors constexpr float_decomposition() : float_decomposition(decompose_float(T{})) {} // [float.decompose.accessors], accessors constexpr int sign() const noexcept; constexpr int exponent() const noexcept; constexpr significand_type significand() const noexcept; constexpr explicit operator value_type() const noexcept; // [float.decompose.boolean], boolean properties constexpr bool is_finite() const noexcept; constexpr bool is_inf() const noexcept; constexpr bool is_nan() const noexcept; constexpr bool is_normal() const noexcept; constexpr bool sign_bit() const noexcept; constexpr bool is_signaling() const noexcept; constexpr bool is_subnormal() const noexcept; constexpr bool is_zero() const noexcept; };

float_decomposition objects store information about individual components of the value representation of an object of floating-point type T. This value representation is called the underlying value of the float_decomposition object. Copying and assigning such objects copies and assigns their underlying value, respectively.

¶ A specialization of float_decomposition 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 float_decomposition or instantiates a specialization float_decomposition<T> where T 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 bfloat16_t, if provided.

[float.decompose.properties]

Nested under [float.decompose.class], insert a new subclause:

Properties [float.decompose.properties]

static constexpr int min_exponent;

¶ The lowest possible result of exponent().

static constexpr int max_exponent;

¶ The greatest possible result of exponent().

static constexpr int exponent_width;

¶ The minimum required width for a signed integer type to represent both min_exponent and max_exponent.

using significand_type = see below;

¶ An implementation-defined unsigned integer type ([basic.fundamental]) capable of representing all possible results of significand(). A specialization of float_decomposition<T> is ill-formed if no such integer type exists.

static constexpr significand_type max_significand;

¶ The greatest possible result of significand().

[Note: This includes results of significand() where implicit bits have been concatenated to significand bits present in the value representation of an underlying value. If T is an ISO/IEC 60559 binary interchange floating-point type, significand_width equals numeric_limits<T>::digits + 1. — end note]

[float.decompose.accessors]

Nested under [float.decompose.class], insert a new subclause:

Accessors [float.decompose.accessors]

constexpr int sign() const noexcept;

Returns: sign_bit() ? -1 : 1.

constexpr int exponent() const noexcept; constexpr significand_type significand() const noexcept;

¶ Let x be the underlying value. Let b be radix.

Effects:

  • If x is finite, exponent() returns an integer e and significand() returns an integer m, such that | x | = m × be . Which pair of e and m is chosen is a property of the implementation, except that
    • m is chosen to be exactly representable in type T,
      [Example: If T is std::float16_t and x is 1, the greatest possible result of significand() satisfying this requirement is 65'536, where exponent() returns -16. — end example]
    • if T is an ISO/IEC 60559 binary interchange floating-point type, e=E−bias, where E is the biased exponent encoded in x, and bias is the exponent bias constant for the format, as specified in ISO/IEC 60559.
      [Note: Consequently, the following statements are true:
      • If and only if x is zero or subnormal, exponent() returns min_exponent.
      • There is only one possible choice of significand().
      • If for two float_decomposition objects a and b, a.exponent() > b.exponent() is true and neither of their underlying values is NaN, value_type(a) > value_type(b) is true.
      — end note]
    • if T is an ISO/IEC 60559 decimal interchange floating-point type, e is the quantum exponent of x.
      [Example: A floating-point value 1.5 of an ISO/IEC 60559 decimal64 floating-point type can be decomposed so that significand() returns 15 and exponent() returns -1 (corresponding to 15×10-1), or so that significand() returns 150 and exponent() returns -2 (corresponding to 150×10-2), where radix equals 10. The result of exponent() is equal to that of the C standard library function llquantexpd64. — end example]
    [Note: Ignoring subnormal flushing, x equals copysign(scalbn(significand(), exponent()), T(sign())). — end note]
  • Otherwise, if T is an ISO/IEC 60559 binary interchange floating-point type, exponent() returns a result one greater than exponent() returns for any finite x, and significand() returns the significand bits in the value representation of x, reinterpreted as an integer.
    [Note: If x is negative or positive infinity, significand() returns zero. If x is NaN, significand() returns the NaN payload. — end note]
  • Otherwise, the result of exponent() and significand() is derived from x in an implementation-defined manner.

Remarks: If two float_decomposition objects have the same underlying value, their respective exponent() and significand() results are equal.

Recommended practice: If T is not an ISO/IEC 60559 binary interchange floating-point type but has similar representation, implementations should return the same results as if T were an ISO/IEC 60559 binary interchange floating-point type.

SEE ALSO: ISO/IEC 9899:2024 7.12.15.4

constexpr explicit operator value_type() const noexcept;

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 T is an ISO/IEC 60559 decimal interchange floating-point type, the result has the same quantum exponent as the underlying value. signbit(static_cast<value_type>(*this)) equals sign_bit().

The Recommended practice: paragraph addresses the case of x87 80-bit long double, 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 <cmath> functions have been adopted from C23 via [P3935R2].

Boolean accessors [float.decompose.boolean]

constexpr bool is_finite() const noexcept; constexpr bool is_inf() const noexcept; constexpr bool is_nan() const noexcept; constexpr bool is_normal() const noexcept; constexpr bool sign_bit() const noexcept; constexpr bool is_signaling() const noexcept; constexpr bool is_subnormal() const noexcept; constexpr bool is_zero() const noexcept;

Returns: test-fun(x), where test-fun is the corresponding function in <cmath> with the underscore in the name removed, and x is the underlying value.

[float.decompose.function]

Nested under [float.decompose.class], insert a new subclause:

Function decompose_float [float.decompose.function]

template<class T> constexpr float_decomposition<T> decompose_float(const T& x) noexcept;

Constraints: T is a cv-unqualified floating-point type.

Returns: A float_decomposition<T> object whose underlying value is x.

Remarks: No floating-point exceptions are raised, and no inexact operation takes place.

6. References

[N3220] JeanHeyd Meneide. ISO/IEC 9899:2024 Working Draft 2024-02-22 https://www.open-std.org/jtc1/sc22/wg14/www/docs/n3220.pdf
[N5054] Thomas Köppe. Working Draft, Programming Languages — C++ 2026-07-16 https://open-std.org/jtc1/sc22/wg21/docs/papers/2026/n5054.pdf

Appendix — Sample implementations

decompose_float(float64_t)

struct f64_decomposition { private: static constexpr int exponent_bits = 11; static constexpr int significand_bits = 52; static constexpr int exponent_offset = 1023 + significand_bits; static constexpr uint64_t integer_bit = 1ull << significand_bits; static constexpr uint64_t significand_mask = integer_bit - 1; public: static constexpr int radix = 2; static constexpr int min_exponent = -1023 - significand_bits; static constexpr int max_exponent = 1024 - significand_bits; static constexpr int exponent_width = exponent_bits + 1; static constexpr uint64_t max_significand = (integer_bit << 1) - 1; static constexpr int significand_width = significand_bits + 1; using significand_type = uint64_t; uint64_t m; int e; bool s; constexpr int sign() const noexcept { return s ? -1 : 1; } constexpr bool sign_bit() const noexcept { return s; } constexpr int exponent() const noexcept { return e; } constexpr significand_type significand() const noexcept { return m; } constexpr bool is_finite() const noexcept { return e != max_exponent; } constexpr bool is_inf() const noexcept { return e == max_exponent && m == 0; } constexpr bool is_nan() const noexcept { return e == max_exponent && m != 0; } constexpr bool is_normal() const noexcept { return e != max_exponent && e != min_exponent; } constexpr bool is_signaling() const noexcept { return e == max_exponent && m != 0 && (m & (integer_bit >> 1)) == 0; } constexpr bool is_subnormal() const noexcept { return e == min_exponent && m != 0; } constexpr bool is_zero() const noexcept { return e == min_exponent && m == 0; } private: static constexpr f64_decomposition decompose(float64_t x) noexcept { constexpr int max_raw_exp = (1 << exponent_bits) - 1; auto xi = std::bit_cast<uint64_t>(x); int raw_exp = (xi >> significand_bits) & max_raw_exp; bool signbit = (xi >> (exponent_bits + significand_bits)) != 0; uint64_t raw_m = xi & significand_mask; if (raw_exp == 0) [[unlikely]] { // Subnormals in binary interchange formats // have the same mathematical exponent as the smallest normal numbers. // Since we seemingly violate that // by subtracting exponent_offset unconditionally later, // we must shift to the left by 1 here, // which has the same effect as incrementing the exponent for subnormals. // // The benefit is that subnormals and zeros can be identified // by their exponent alone, // i.o.w. every distinct exponent in the representation // also results in a distinct exponent() result. raw_m <<= 1; } else if (raw_exp != max_raw_exp) [[likely]] { // Normal numbers (but not infinity or NaN) have an implicit leading 1. raw_m |= integer_bit; } return { raw_m, raw_exp - exponent_offset, signbit }; } public: constexpr explicit operator float64_t() const noexcept { uint64_t raw_exp = e + exponent_offset; // Subnormal significands and zeroes have been shifted to the left above. uint64_t raw_m = m >> (e == min_exponent); uint64_t bits = (uint64_t{s} << (exponent_bits + significand_bits)) | (raw_exp << significand_bits) | (raw_m & significand_mask); return std::bit_cast<float64_t>(bits); } friend constexpr f64_decomposition decompose_float(float64_t x); }; constexpr f64_decomposition decompose_float(float64_t x) { return f64_decomposition::decompose(x); }

clang++-24 -O3 -march=x86-64-v3 output (labels adjusted for readability):

decompose_float: vmovq rcx, xmm0 mov rdx, rcx shr rdx, 52 mov al, 52 bzhi rax, rcx, rax and edx, 2047 je .subnormal_or_zero cmp edx, 2047 je .skip_adding_integer_bit movabs rsi, 4503599627370496 or rax, rsi .skip_adding_integer_bit: add edx, -1075 shr rcx, 31 movabs rsi, 4294967296 and rsi, rcx or rdx, rsi ret .subnormal_or_zero: add rax, rax jmp .skip_adding_integer_bit

frexp(float64_t, int*)

constexpr float64_t frexp(float64_t x, int *e) { auto xi = std::bit_cast<uint64_t>(x); int raw_exp = (xi >> 52) & 0x7ff; if (raw_exp == 0x7ff) [[unlikely]] { // infinity or NaN return x; } if (raw_exp == 0) [[unlikely]] { // subnormals or zero if (x) [[unlikely]] { // subnormal numbers xi = std::bit_cast<uint64_t>(x * 0x1p64); raw_exp = ((xi >> 52) & 0x7ff) - 64; } else { // exactly zero *e = 0; return x; } } *e = raw_exp - 0x3fe; xi &= 0x800fffffffffffffull; // exponent bits = 0 xi |= 0x3fe0000000000000ull; // exponent bits |= 0x3fe return std::bit_cast<double>(xi); }

clang++-24 -O3 -march=x86-64-v3 output (labels adjusted for readability):

.x1p64: .quad 0x43f0000000000000 frexp: vmovq rax, xmm0 mov rcx, rax shr rcx, 52 and ecx, 2047 cmp ecx, 2047 je .inf_or_nan test ecx, ecx je .subnormal_or_zero .common_exit: add ecx, -1022 mov dword ptr [rdi], ecx movabs rcx, -9218868437227405313 and rcx, rax movabs rax, 4602678819172646912 or rax, rcx vmovq xmm0, rax .inf_or_nan: ret .subnormal_or_zero: vxorpd xmm1, xmm1, xmm1 vucomisd xmm0, xmm1 jne .subnormal jp .subnormal mov dword ptr [rdi], 0 ret .subnormal: vmulsd xmm0, xmm0, qword ptr [rip + .x1p64] vmovq rax, xmm0 mov rcx, rax shr rcx, 52 and ecx, 2047 add ecx, -64 jmp .common_exit

frexpq(float64_t, int*)

constexpr float64_t frexpq(float64_t x, int *e) { auto xi = std::bit_cast<uint64_t>(x); int raw_exp = (xi >> 52) & 0x7ff; if (raw_exp == 0x7ff) [[unlikely]] { // infinity or NaN return x; } if (raw_exp == 0) [[unlikely]] { // subnormals or zero if (x) [[unlikely]] { // subnormal numbers xi = std::bit_cast<uint64_t>(x * 0x1p64); raw_exp = ((xi >> 52) & 0x7ff) - 64; } else { // exactly zero *e = 0; return x; } } *e = raw_exp - 0x433; xi &= 0x800fffffffffffffull; // exponent bits = 0 xi |= 0x4330000000000000ull; // exponent bits |= 0x433 return std::bit_cast<double>(xi); }

clang++-24 -O3 -march=x86-64-v3 output (labels adjusted for readability):

.x1p64: .quad 0x43f0000000000000 frexpq: vmovq rax, xmm0 mov rcx, rax shr rcx, 52 and ecx, 2047 cmp ecx, 2047 je .LBB1_5 test ecx, ecx je .LBB1_2 .common_exit: add ecx, -1075 mov dword ptr [rdi], ecx movabs rcx, -9218868437227405313 and rcx, rax movabs rax, 4841369599423283200 or rax, rcx vmovq xmm0, rax .inf_or_nan: ret .subnormal_or_zero: vxorpd xmm1, xmm1, xmm1 vucomisd xmm0, xmm1 jne .subnormal jp .subnormal mov dword ptr [rdi], 0 ret .subnormal: vmulsd xmm0, xmm0, qword ptr [rip + .x1p64] vmovq rax, xmm0 mov rcx, rax shr rcx, 52 and ecx, 2047 add ecx, -64 jmp .common_exit