Floating-point std::saturating_cast
- Document number:
- P4355R0
- Date:
2026-08-30 - 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/P4355/github
- Source:
- github.com/eisenwave/cpp-proposals/blob/main/src/float-saturating-cast.cow
should accept floating-point operands.
Contents
Introduction
Hardware support
Comparison with other languages
The problem
Proposal
Motivation
Quantization
Optimal output using Rust
Low-quality output using C++
Safety
Engine implementation
Design
Strategy
Handling of NaN
Floating-point exceptions and constant expressions
Other saturating operations
std::simd overloads
Impact on existing code
Implementation experience
Possible implementation
Wording
[version.syn]
[numeric]
[simd]
References
1. Introduction
C++ is currently an outlier among programming languages in terms of the conversion between floating-point types and integer types. The behavior is specified in [conv.fpint] paragraph 1:
A prvalue of a floating-point type can be converted to a prvalue of an integer type. The conversion truncates; that is, the fractional part is discarded. The behavior is undefined ([ub:conv.fpint.float.not.represented]) if the truncated value cannot be represented in the destination type.
Notably, any infinities and NaNs result in UB, and any value outside the representable range (possibly as the result of some numerical instability) results in UB.
1.1. Hardware support
The status quo actually makes sense because it allows for high performance on
a variety of architectures.
Mainstream architectures typically have an instruction for converting
floating-point numbers to integers.
Let's focus on the conversion from to .
| Input | (x86_64) |
(RISC-V) |
(AArch64) |
(WASM) |
|---|---|---|---|---|
|
|
|
|
|
|
|
|
|
|
| small | |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Because everything but the handling of small values which are actually representable
as the resulting integer is UB,
the conversion can always be lowered to a single instruction.
results on x86_64 are just an error indicator.
The IA flag is also raised, indicating an invalid instruction.
For RISC-V, the assumption is that the (round to zero)
rounding mode was explicitly specified.
1.2. Comparison with other languages
Similar to comparing hardware capabilities, we can also compare how other languages handle this conversion:
| Input | C++ | Rust | Java | C# (checked) | C# (unchecked) |
|---|---|---|---|---|---|
| UB | |
|
exception thrown | unspecified | |
| UB | |
|
exception thrown | unspecified | |
| small | |
|
|
|
|
| UB | |
|
exception thrown | unspecified | |
| UB | |
|
exception thrown | unspecified | |
| UB | |
|
exception thrown | unspecified |
The key insight is that the saturating behavior is considered so useful that it is baked into the core language of Rust and Java.
1.3. The problem
The problem is that making almost every part of the tables above undefined behavior is not very useful for some domains. It is fairly common that when quantizing floating-point values to integers, some values are outside the representable range, especially in the domain of signal processing.
In that domain, saturating behavior would be much more useful.
Frustratingly, we have a function,
but that function is only defined for integer types.
1.4. Proposal
The way to solve the problem would be to extend
to also support floating-point types.
It would also be possible to make changes to the core language,
aligning C++ with Rust and Java,
but this would have considerable performance cost
which massive amounts of existing code would silently incur.
Clang users would all have to start using
2. Motivation
We already see some evidence that a saturating cast from floating-point to integer is useful. This is the behavior of the core-language conversion in Rust and Java, and AAarch64 has an instruction which implements this behavior exactly.
In C++, one can similarly imagine many applications for this behavior:
2.1. Quantization
It is common in calculations related to RGB colors to represent the linearized
color values as floating-point numbers in the range [0, 1].
When stored as pixel values,
they are typically quantized to 8-bit integers.
It is also common for some values to fall outside the [0, 1] range
due to numerical instability or because the original color space had a wider gamut.
That makes the UB in the conversion from to a safety hazard.
While the problem can be solved by clamping to the range [0, 1] before the conversion,
this is pessimistic because on e.g. ARM,
the underlying
2.1.1. Optimal output using Rust
Looking at
The output for shows that
was simply lowered to a
2.1.2. Low-quality output using C++
C++ developers are essentially forced to get the output
(see https://godbolt.org/z/dqofdPW7a)
because without clamping,
they would run into UB for infinities and large values:
The frustrating part is that the theoretical optimum
on ARM is known,
but there is no way to express it in the C++ language without UB.
Getting to the optimal compiler output simply through optimizations
also seems unrealistic.
If accepted floating-point types,
we could go straight from to ,
producing the same output as Rust's conversion.
2.2. Safety
Another obvious problem with a language facility that has so much UB is safety.
If someone wanted to avoid core language UB,
conversions would present a minefield,
and there is no clear alternative.
is that safer alternative:
whenever a C++ developer is unsure whether a conversion is safe,
they can use to avoid UB.
For the conversions that are already well-defined,
this results in no change in behavior,
and on platforms like AAarch64 or WASM,
it results in no performance cost either.
2.3. Engine implementation
The saturating behavior is gradually becoming the de-facto standard for how to handle conversions from floating-point to integer types. Languages like Rust and Java handle conversions like this and architectures like AAarch64 and WASM have instructions for this behavior. That makes it increasingly relevant for C++ to have such an operation as well, to make the implementation of compilers, WASM runtimes, emulators, etc. easier and more efficient.
3. Design
3.1. Strategy
The overall strategy for
is to match the Rust and Java behavior,
as well as the
3.2. Handling of NaN
The proposed behavior is to return zero for NaN inputs, which matches the aforementioned precedent.
One could also match the RISC-V is for the original input,
the result of the saturating cast also provides information about which
non-finite value was passed in
(, , or ).
3.3. Floating-point exceptions and constant expressions
The handling of floating-point exceptions should be
consistent with the rest of the standard library,
such as any functions in ,
and should try to be consistent with the
Invalid Operation
Occurs if the floating-point input is a NaN, infinity, or a numerical value that cannot be represented in the destination register. An out of range integer or fixed-point result is saturated to the size of the destination register.
Inexact
Occurs if the numeric result that differs from the input value.
Curiously, C's Annex F leaves it unspecified whether a
conversion raises the inexact
exception,
so out of caution and to grant some implementation freedom,
we should not mandate it either.
A domain error should only take if the input is NaN.
Otherwise, is quasi-useless during constant evaluation.
and would be equivalent in that case
because everything that is UB in
turns into a domain error in ,
and domain errors disqualify expressions from being constant expressions in the standard library.
It also doesn't make any design sense for an operation that is explicitly saturating
to report a domain error when it saturates.
3.4. Other saturating operations
The rest of the saturation arithmetic library is not affected.
That is, there is no floating-point support for etc.
These functions are simply not in scope for the paper,
and the motivation does not apply to them because floating-point operations
are already saturating
(in the sense that they clamp to and ).
I also don't propose floating-point types as a result of
because converting to floating-point types is already quasi-saturating.
3.5. std::simd overloads
[P2956R3] proposes overloads for saturation arithmetic,
including .
At the time of writing,
the paper is in LWG, so we expect it to be included in C++29.
Naturally, should also support floating-point types.
4. Impact on existing code
The proposed change only relaxes the constraints on
to allow floating-point types.
Existing uses with integer operands are not affected.
5. Implementation experience
The proposed behavior is available in LLVM as the
5.1. Possible implementation
A pure library implementation with some statements for range checks
is theoretically possible, but a waste of time from an implementer viewpoint.
The goal is to emit exactly one e.g.
6. Wording
The changes are relative to [N5054].
[version.syn]
In [version.syn], bump the feature-test macro.
[numeric]
Change [numeric.sat.cast] as follows:
Constraints:
and is a
signed or unsigned integer type ares ([basic.fundamental]).
is a signed or unsigned integer type or
a cv-unqualified floating-point type.
Returns:
If is NaN, zero.
Otherwise, let be the value of
with the fractional part (if any) discarded.
If
is representable as a value of type ,
return ;
otherwise, returns either the largest or smallest representable value of type ,
whichever is closer to
the value of .
Remarks:
If and only if is is a floating-point type,
floating-point exceptions may be raised,
unless the value of is exactly representable in type .
Whether the inexact
exception is raised is a property of the implementation.
A domain error occurs if is NaN,
in which case a function call expression is not a constant expression.
SEE ALSO: ISO/IEC 9899:2024 7.12.1
[simd]
If [P2956R3] has been applied, change [simd.syn] as follows:
If [P2956R3] has been applied, change [simd.alg] as follows:
Constraints:
Both and
is a
signed or unsigned integer type ares ([basic.fundamental]).
is
a signed or unsigned integer type or
a cv-unqualified floating-point type.
Returns:
A where the element is
initialized to the result of
for all in the range [, ).