1

I am getting some -Wnarrowing conversion errors when doubles are narrowed to floats. How can I do this in a well defined way? Preferably with an option in a template I can toggle to switch behavior from throwing exceptions, to clamping to the nearest value, or to simple truncation. I was looking at the gsl::narrow cast, but it seems that it just performs a static cast under the hood and a comparison follow up: Understanding gsl::narrow implementation. I would like something that is more robust, as according to What are all the common undefined behaviours that a C++ programmer should know about? static_cast<> is UB if the value is unpresentable in the target type. I also really liked this implementation, but it also relies on a static_cast<>: Can a static_cast<float> from double, assigned to double be optimized away? I do not want to use boost for this. Are there any other options? It's best if this works in c++03, but c++0x(experimental c++11) is also acceptable... or 11 if really needed...

Because someone asked, here's a simple toy example:

#include <iostream>

float doubleToFloat(double num) {
    return static_cast<float>(num);
}

int main( int, char**){
    double source = 1; // assume 1 could be any valid double value
    try{
        float dest = doubleToFloat(source);
        std::cout << "Source: (" << source << ") Dest: (" << dest << ")" << std::endl;
    }
    catch( std::exception& e )
    {
        std::cout << "Got exception error: " << e.what() << std::endl;
    }
}

My primary interest is in adding error handling and safety to doubleToFloat(...), with various custom exceptions if needed.

HolyBlackCat
  • 78,603
  • 9
  • 131
  • 207
alrav
  • 117
  • 8
  • Please put your code in the question. – Casey Nov 15 '22 at 19:34
  • @Casey added an example – alrav Nov 15 '22 at 19:43
  • A `double` is not representable as `float` iff its absolute value is greater than `FLT_MAX`. – n. m. could be an AI Nov 15 '22 at 19:52
  • @n.m. Yes, but I don't think that's enough is it? Don't you also need to take into account loss of precision? And negative numbers, but I feel a few abs() calls to make everything positive will probably resolve that... – alrav Nov 15 '22 at 19:58
  • 1
    @n.m. There are plenty of `double` values in between consecutive `float` values. [Demo](https://godbolt.org/z/r9z6433Mn). Do they not count as "not representable"? – Nelfeal Nov 15 '22 at 20:12
  • @Nelfeal s/not representable/not convertible/. "If the source value can be exactly represented in the destination type, the result of the conversion is that exact representation. If the source value is between two adjacent destination values, the result of the conversion is an implementation-defined choice of either of those values. Otherwise, the behavior is undefined." – n. m. could be an AI Nov 15 '22 at 20:50
  • @alrav Converting `double` to `float` loses precision, that's a given. What do you want to do about it? – n. m. could be an AI Nov 15 '22 at 20:52
  • @n.m. I want to control the rounding/clamping behavior where applicable, and optionally throw an exception. If it's implementation defined, that is unfortunate, and I consider UB to be unacceptable in my code. – alrav Nov 15 '22 at 22:25

2 Answers2

0

It depends on what you mean by "safely". There will most likely be a drop of precision in most cases. Do you want detect if this happens? Assert, or just know about it and notify the user?

A possible path of solution would be to static casting the double to a float, and then back to double, and compare the before and after. Equality is unlikely, but you could assert that the loss of precision is within your tolerance.

float doubleToFloat(double a_in, bool& ar_withinSpec, double a_tolerance) 
{
    auto reducedPrecision = static_cast<float>(a_in);
    auto roundTrip = static_cast<double>(reducedPrecision);
    ar_withinSpec = (roundTrip < a_tolerance);
    return reducedPrecision;
}
NGauthier
  • 885
  • 8
  • 17
  • This is similar to what gsl does, but I am concenred about invoking UB during the static cast as if there is a loss of precision, that means that the value is not representable in the target type. That said, if this could be detected and not otherwise handled, an exception would be acceptable. I do not want to use an assert here. – alrav Nov 15 '22 at 22:03
0

As long as your floating-point types can store infinities (which is extremely likely), there is no possible undefined behavior. You can test std::numeric_limits<float>::has_infinity if you really want to be sure.

Use static_cast to silence the warning, and if you want to check for an overflow, you can do something like this:

template <typename T>
bool isInfinity(T f) {
    return f == std::numeric_limits<T>::infinity()
        || f == -std::numeric_limits<T>::infinity();
}

float doubleToFloat(double num) {
    float result = static_cast<float>(num);
    if (isInfinity(result) && !isInfinity(num)) {
        // overflow happened
    }
    return result;
}

Any double value that doesn't overflow will be converted either exactly or to one of the two nearest float values (probably the nearest). You can explicitly set the rounding direction with std::fesetround.

Nelfeal
  • 12,593
  • 1
  • 20
  • 39
  • This seems very promising. Is this safe for multithreading though? I don't see that mentioned in the link for std::fesetround. Can I count on this being thread local? Or do I need additional synchronization to utilize this? – alrav Nov 15 '22 at 22:07
  • 1
    I don't understand what you're asking. Is `fesetround` changes thread-local? [It seems so](https://stackoverflow.com/questions/14742784/is-c99-fesetround-fegetround-state-per-thread-or-per-process). Is `doubleToFloat` thread-safe? Yes, it's a pure function. – Nelfeal Nov 15 '22 at 22:13
  • Yes, I was wondering if fesetround changes are thread-local. I will accept this answer with that context. – alrav Nov 15 '22 at 22:22