Home > Net >  Bitwise comparing NaN's in C
Bitwise comparing NaN's in C

Time:12-03

I have a function that returns a double. Any real number is a valid output. I'm using nan's to signal errors. I am error checking this way.

double foo();

const auto error1 = std::nan("1");
const auto error2 = std::nan("2");
const auto error3 = std::nan("3");

bool bit_equal(double d1, double d2) {
    return *reinterpret_cast<long long*>(&d1) == *reinterpret_cast<long long*>(&d2);
}

const auto value = foo();
if(std::isnan(value)) {
    if (bit_equal(value, error1)) /*handle error1*/;
    else if (bit_equal(value, error1)) /*handle error2*/;
    else if (bit_equal(value, error1)) /*handle error3*/;
    else /*handle default error*/;
} else /*use value normally*/;

Alternatively, if the compiler support has caught up, I can write it this way

double foo();

constexpr auto error1 = std::nan("1");
constexpr auto error2 = std::nan("2");
constexpr auto error3 = std::nan("3");

constexpr bool bit_equal(double d1, double d2) {
    return std::bit_cast<long long>(d1) == std::bit_cast<long long>(d2);
}

const auto value = foo();
if(std::isnan(value)) {
    if (bit_equal(value, error1)) /*handle error1*/;
    else if (bit_equal(value, error1)) /*handle error2*/;
    else if (bit_equal(value, error1)) /*handle error3*/;
    else /*handle default error*/;
} else /*use value normally*/;

Or even

double foo();

constexpr auto error1 = std::bit_cast<long long>(std::nan("1"));
constexpr auto error2 = std::bit_cast<long long>(std::nan("2"));
constexpr auto error3 = std::bit_cast<long long>(std::nan("3"));

const auto value = foo();
if(std::isnan(value)) {
    switch(std::bit_cast<long long>(value)) {
    case error1: /*handle error1*/; break;
    case error1: /*handle error2*/; break;
    case error1: /*handle error3*/; break;
    default: /*handle default error*/;
    }
} else /*use value normally*/;

I have to do this because comparing nan's with == always returns false.

  1. Is there a standard function to perform this comparison in C ?
  2. Are any of these 3 alternatives better than the others? Although the last option seems the most succinct, it requires me to do return std::bit_cast<double>(error1); inside foo() rather than just return error1;.
  3. Is there a better design where I can avoid using nan as an error value?

CodePudding user response:

  1. Is there a better design where I can avoid using nan as an error value?

Yes.

  1. Throw an exception.
  2. Use a struct (or tuple, not really) as return value
  3. Use an out ref parameter

Since there are better alternatives, I don't think it's worth answering question 1. and 2.

CodePudding user response:

Returning NaNs as error indicators is certainly a valid design choice. If you write numeric code, I'm sure you will find many people who get annoyed when you throw exceptions on any invalid input instead of letting the error propagate through NaNs. "When in Rome, speak like the Romans", right? When in math, speak like the math.h functions ;-)

(Of course this depends on your use case and the expectations of your API users)

However, NaN payloads aren't that good. Using them as an error "hint" may work for you, so you can look at the payload in a data dump and find out where it came from. But as you certainly have noticed, there is no predefined inverse to nan(const char*). Also, NaN payloads tend not to propagate well. For example, while most math functions will return a NaN when they received a NaN input, they will give you a new one without the payload.

There is a good article by agner.org talking about this very topic: Floating point exception tracking and NAN propagation

My personal recommendation would be:

  1. Keep returning NaN on error because it is fast to check
  2. Keep using payloads as error hints
  3. Use a different mechanism to signal the specific type of error

Options that come to mind:

  1. Exceptions. Maybe paired up with a non-throwing variant for users that are content with just a NaN
double foo();
double foo(std::nothrow_t) noexcept;

double bar()
{
    try {
        double x = foo();
    } except(const std::domain_error&) {
        error();
    }
    double y;
    if(std::isnan(y = foo(std::nothrow)))
        error();
}
  1. Optional error code or struct output argument: double foo(Error* error=nullptr). After the call, check for NaN. If NaN, read exact error from error struct. If the user is not interested in the exact error, they don't pass a struct to begin with
struct Error
{
    int errcode;
    operator bool() const noexcept
    { return errcode; }

    /** throw std::domain_error with error message */
    [[noreturn]] void raise() const;

    void check() const
    {
        if(errcode)
            raise();
    }
}
double foo(Error* err=nullptr) noexcept;

double bar()
{
    Error err;
    double x;
    x = foo(); // just continue on NaN
    if(std::isnan(x = foo()))
        return x; // abort without error explanation
    if(std::isnan(x = foo(&err)))
        err.raise(); // raise exception
    return x;
}
  1. std::variant<double, Error> return value. In my opinion the API is not well suited for this; too verbose. This will be fixed in C 23 with std::expected. Also less efficient because the data will likely be returned on the stack
  2. std::pair<double, Error>. If the Error type is a simple struct without a destructor and with a maximum size of 8 byte or a primitive type (so it can be returned in a register), this will be very efficient and it is also easy to check. Building your own custom pair-like type that offers some convenience methods like get_result_or_throw_error() is also possible.
template<class T>
struct Result
{
    T result;
    Error err;

    Result() = default;
    explicit constexpr Result(T result) noexcept
    : result(result),
      err() // set to 0
    {}
    explicit constexpr Result(Error err, T result=NAN) noexcept
    : result(result),
      err(err)
    {}
    operator bool() const noexcept
    { return err; }

    T check() const
    {
        err.check(); // may throw
        return result;
    }
    bool unpack(T& out) const noexcept
    {
        if(err)
            return false;
        out = result;
        return true;
    }
};

Result<double> foo() noexcept;

double bar()
{
    double x = foo().check(); // throw on error
    double y = foo().result; // ignore error. Continue with NaN
}
Result<double> baz() noexcept
{
    Result<double> rtrn;
    double x;
    if(! (rtrn = foo()).unpack(x))
        return rtrn; // propagate error
    rtrn.result = x   1.; // continue operation
    return rtrn;
}
  • Related