c++error-handlingnanc++20bit-cast

Bitwise comparing NaN's in C++


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?

Solution

  • NaN error signaling

    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

    Alternative mechanisms

    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;
    }
    

    Further discussion

    To give a bit more of a personal opinion and also delve into a few more performance concerns:

    Exceptions

    Well, all the usual aspects of exception handling and when to use them apply. See for example When and how should I use exception handling?

    I think at this point the general consensus on exceptions is that they should not be part of the regular control flow and should only be used for very rare, exceptional cases where you most likely want to abort the operation instead of, say mitigating the error. It is just too easy to forget catching exceptions on all call sites so they tend to travel very far up the call chain before being caught.

    So their use is very situational. Do you want your users to explicitly deal with any error condition on the site where they appear? Then don't use exceptions because users of your API will definitely not be bothered using a try-except block everywhere. If you want the error to get out of the way as far as possible, use them.

    As for the idea of using a second set of functions without exceptions: Well, it doesn't compose well. It's feasible for a small set of functions but do you really want to write every piece of code twice, once with and once without exceptions? Probably not.

    Error output parameter

    This is probably the most flexible option while remaining very efficient. Passing an additional parameter has a minor cost but it isn't too bad.

    The main benefit is that this is the only option besides exceptions that allows you to compose complex error reports with dynamic memory allocation for error messages etc. without incurring extra costs in the no-error case. If you make the Result object complex enough to require a destructor, it will be passed on the stack and you need to re-read the error code and actual result value after every function call and then its destructor will run.

    In contrast, the Error object will be rarely touched. Yes, its destructor will run once it goes out of scope. However, I expect that most code bases will have one error object very far up the call chain and then just pass it down and reuse that object as needed.

    If you make the Error object complex you might find yourself in a situation where a caller wants the error code but not the error message, e.g. because they expect an error and want to mitigate it instead of reporting it. For this case, it might make sense to add a flag to the object to indicate that the error message should not be filled.

    struct Error
    {
        int errcode;
        bool use_message;
        std::string message;
    };
    

    variant, expected

    I think I've made it sufficiently clear above that I don't think std::variant has a suitable API for this task. std::expected may one day be available on every platform you target but right now it isn't and you will definitely draw the ire of your release engineers if you start using C++23 features and they have to build your code for RHEL-8 or something similarly long-lived.

    Performance-wise all the points I discuss below for Result apply. In addition, the floating point result will always be returned either on the stack or in a general purpose register. Using the Result or std::pair approach will at least get double results in a floating point register on Mac/Linux/BSD, which is a minor advantage, but not huge. floats will still be packed in a GP register, though.

    Result type

    From an API design perspective, the nice thing about a Result object is that the caller cannot ignore the possibility of an error. They may or may not remember to check for NaN or catch exceptions but with Result, they always have to unpack the contained value and in doing so, decide on their desired error handling.

    From a performance perspective, the main point when writing a Result type is that you don't want to make it more expensive to access the actual return value unless you don't care about runtime and code size. This means making sure the return value can be passed in registers instead of the stack.

    On Windows this is very hard to achieve because the Windows calling convention only uses a single register for return objects and I don't think they pack two 32 bit values into one 64 bit register. At this point your only options are a) accept the cost of stack return values b) try to pack error code and result value in one scalar like you did with NaN payloads or other tricks like negative integers c) not use this approach.

    On all other major x86-64 platforms, you have two registers to work with. This is far more feasible unless you regularly return 16 byte payloads like std::complex<double>.

    However, for this to work, the Result must not have a non-trivial destructor or copy/move constructor. For all intents and purposes, this means you cannot have dynamic error messages in the Error type. There are ways around this, if you absolutely need: You enforce that every access to the actual result also checks the error and deallocates, either reporting or ignoring it in the process. Use [[nodiscard]] on the return values to ensure the return value is checked at all. This works, for example:

    struct Error
    {
        std::string* message;
    private:
        [[noreturn]] static void raise_and_delete_msg(std::string*);
    public:
        /*
         * Note: clang needs always_inline to generate efficient
         * code here. GCC is fine
         */
        [[noreturn, gnu::always_inline]] void raise() const
        { raise_and_delete_msg(message); }
    
        void discard() const noexcept
        { delete message; }
    
        operator bool() const noexcept
        { return message != nullptr; }
    
        void check() const
        {
            if(message)
                raise();
        }
    };
    
    template<class T>
    class Result
    {
        T result;
        Error err;
    public:
        constexpr Result()
        : result(),
          err()
        {}
        explicit Result(T result)
        : result(std::move(result)),
          err()
        {}
        /** Takes ownerhip of message. Will delete */
        explicit Result(std::unique_ptr<std::string>&& message)
        : err(Error{message.release()})
        {}
        Result(std::unique_ptr<std::string>&& message, T invalid)
        : result(std::move(invalid)),
          err(Error{message.release()})
        {}
        T unchecked() noexcept
        {
            err.discard();
            return std::move(result);
        }
        T checked()
        {
            err.check();
            return std::move(result);
        }
        bool unpack(T& out) noexcept
        {
            if(err) {
                err.discard();
                return false;
            }
            out = std::move(result);
            return true;
        }
    };
    
    [[nodiscard]] Result<double> foo();
    
    double bar()
    {
        return foo().checked() + 1.;
    }
    

    However, at this point you quickly reach the point where you exceed the 8 bytes you can reasonably use for sizeof(Error) before you go back to stack return values so I'm not sure this is worth it. For example if you want and error code plus message, you need to dynamically allocate both or do other fancy tricks. Plus, [[nodiscard]] is only a warning, so you can still easily get memory leaks.

    Conclusion

    If I have to make suggestions:

    1. Use exceptions if a) they are in line with the coding style and API you normally use plus b) the expectations that both you and your API users have on these functions and c) failure should be rare, costly, and loud

    2. Use Error output arguments if you primarily target Windows or if you want complex error reporting with dynamic messages or similar.

    3. Use Result for simple error codes on Linux/Mac or if you want your API users to always make a conscious decision to check or ignore an error. In that case, you may also accept the additional runtime cost associated with complex Error objects or any such object on Windows.