Reputation: 31
I encountered a problem while trying to put a piece of code inside a template function. The code basically performs clamping. Below is a reproducible version of the code.
#include<iostream>
int main(){
constexpr float lower_value{0.0F};
constexpr float upper_value{4294967295.0F};
float value{4294967295.0F};
uint32_t clampedValue{0};
if (value <= lower_value) {
clampedValue = static_cast<decltype(clampedValue)>(lower_value);
} else if (value >= upper_value) {
clampedValue = static_cast<decltype(clampedValue)>( upper_value);
} else {
clampedValue = static_cast<decltype(clampedValue)>(std::round(value));
}
std::cout << clampedValue << std::endl;
return 0;
}
This works fine as long as lower_value
and upper_value
are either const
or constexpr
. Otherwise it outputs 0
.
When I ran the following lines, it gave 0
, 4294967295
and 4294967295
respectively. The reason for 0
is floating point conversion error as 4294967295
is increased by 1
to 4294967296
to fit into 23 bits Mantissa but this falls outside the range of Uint32 and a wrap around occurs which gaves 0
after static_cast<uint32_t>
. But why do I get the correct result when const float
or constexpr float
is used?
float value{4294967295.0F};
constexpr float valueConstexpr{4294967295.0F};
const float valueConst{4294967295.0F};
std::cout << static_cast<uint32_t>(value) << std::endl;
std::cout << static_cast<uint32_t>(valueConstexpr) << std::endl;
std::cout << static_cast<uint32_t>(valueConst) << std::endl;
The if else
logic of above main
program is put inside a template function which takes the following shape.
#include <iostream>
template <typename A, typename B, typename C, typename D>
constexpr auto process_value(const B& inVal, const C& lower_bound, const D& upper_bound) {
A outVal;
if (inVal <= static_cast<B>(lower_bound)){
outVal = static_cast<A>(lower_bound);
} else if (inVal >= static_cast<B>(upper_bound)){
outVal = static_cast<A>(upper_bound);
} else {
outVal = static_cast<A>(std::round(inVal));
}
return outVal;
}
int main(){
constexpr float lower_value{0.0F};
constexpr float upper_value{4294967295.0F};
float value{4294967295.0F};
uint32_t clampedValue{0};
clampedValue = process_value<decltype(clampedValue), float, float, float>(value, lower_value, upper_value);
std::cout << clampedValue << std::endl;
return 0;
}
But this also gives 0
as output instead of 4294967295
.
I was expecting both versions to give the same output. Can someone please help me how to fix the template function so it does exactly like in the original code?
Upvotes: 3
Views: 96
Reputation: 5321
Presuming that a float
is IEEE 754 representation.
The largest std::uint32_t
that a float
can hold with value fidelity is 4294967040.0f. Converting that float
to std::uint32_t
is well-defined behavior.
The next after value that a float can hold is 4294967296.0f, which is outside the range of a std::uint32_t
. Converting float
values outside the range of std::uint32_t
is undefined behavior (UB), which ought to be avoided. Because of UB, the computed compile-time value and the computed run-time value might differ.
Why does 4294967295.0f become 4294967296.0f? Because a float
has 24-bits of precision significand (sometimes called mantissa, albeit misnomer), and the representation rounds. (The 24-bits comes from 23-bits of actual data, and 1-bit is an implicit bit that is the "hard coded" value 1 for normalized numbers, and 0 for denormalized numbers or the value zero.)
The highest unit increment float
value is 16777216.0f, after which there are "unit value gaps" of expressibility. Sometimes that's a good bit of information to be aware of as well.
To fix the original posted code, change upper_value:
constexpr float upper_value{ 4294967040.0f };
Upvotes: 3
Reputation: 1940
static_cast<float>(std::numeric_limits<std::uint32_t>::max())
is 4294967296, which is one more than 4294967295, and doesn't fit into a 32bit unsigned integer anymore. Demonstration on Godbolt
The reason for this is that float has a mantissa of only 23 bits, so it can't represent all integers that fit into an uint32_t
exactly. Hence it will round. For example, 4294967167 will be rounded to 4294967040, while 4294967168 will already be rounded to 4294967296.
Hence if you convert your limit to float and then to the clamping, this will fail.
You could use std::nextafter(static_cast<float>(std::numeric_limits<std::uint32_t>::max()), -std::numeric_limits<float>::infinity())
to get the floating point value immediately before that one that still fits in both data types, which would be 4294967040. Any float
value larger than that will automatically get rounded to a value that doesn't fit in uint32_t
.
Upvotes: 0