Muhammad Altaf
Muhammad Altaf

Reputation: 31

static_cast<uint32_t> returns 0 for non-const/constexpr float, holding 2^32 -1 (UINT32_MAX) but returns UINT32_MAX when float is const/constexpr

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

Answers (2)

Eljay
Eljay

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

chris_se
chris_se

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

Related Questions