This is Words and Buttons Online — a collection of interactive #tutorials, #demos, and #quizzes about #mathematics, #algorithms and #programming.

Error codes are not numbers. But they are. Can we exploit that?

People do it with different feelings. Sometimes with pride for a clever solution, sometimes with guilt for an obscure hack. Which is odd since there is nothing hacky about it. The IEEE 754 is ok with it. The ISO/IEC 14882 is ok with it. So why is it perceived as a hack and not a norm?

I'm talking, of course, about storing return codes in floating-point NaNs.

What?

The floating-point numbers are often perceived as esoteric and unusual. For instance, you can't sum up three thirds and expect 1. But that is not due to sorcery, that's due to engineering.

A floating-point number consists of a sign bit, an exponent, and a significant. The model below is a perfectly standard IEEE 754 number, although it was only given 6 bits to make it more clickable.

It is set to indicate the first best "not a number" value. But there's more than one. This particular model has 6 NaNs: 3 with a plus sign, and 3 more with a minus.

A single-precision floating-point number has 16 777 214 different NaNs, and double-precision has 9 007 199 254 740 990. It would be wasteful not to exploit that.

How?

In C++, you can use the significant bits to store error codes by unionizing the enumerator with a floating-point number. Consider this example.

#include <cassert>
#include <cmath>
#include <cstdint>

enum class ECode : uint64_t {
    INPUT_IS_NAN = 0xFFF0'0000'0000'0001,
    INPUT_IS_INFINITE,
    INPUT_IS_NEGATIVE
};

union Result_or_code
{
    double result;
    ECode code;
    Result_or_code(double x) {result = x;}
    Result_or_code(ECode c) {code = c;}
    operator double() {return result;}
    operator ECode() {return code;}
};

Result_or_code sqrt_or_not(double x) {
    if (std::isnan(x))
        return ECode::INPUT_IS_NAN;
    if (std::isinf(x))
        return ECode::INPUT_IS_INFINITE;
    if(x < 0)
        return ECode::INPUT_IS_NEGATIVE;
    return std::sqrt(x);
}

int main(void) {
    assert(sqrt_or_not(-1.) == ECode::INPUT_IS_NEGATIVE);
    assert(sqrt_or_not(1./0.) == ECode::INPUT_IS_INFINITE);
    assert(sqrt_or_not(0./0.) == ECode::INPUT_IS_NAN);
    assert(sqrt_or_not(4.) == 2.);

    assert(std::isnan(sqrt_or_not(-1.)));
    assert(std::isnan(sqrt_or_not(1./0.)));
    assert(std::isnan(sqrt_or_not(0./0.)));
    assert(std::isnan(sqrt_or_not(-1.) + 1.));
}
    

In this example, a square root is being computed for the argument in double. Or it isn't. Not all the doubles have a legitimate square root. The error code enumerator is unionized with the numeric result in double so you get a number when possible and an error code enum when not.

The enumerator starts from the first negative NaN so every error code is also a NaN. And every valid number still has a representation in double. We have both ranges covered with a single entity.

The square root of NaN is NaN. Which makes sense since every number has a square in numbers including +INF.

The square root of +INF is also a NaN. This is a bit controversial since you have a lot of numbers other than +INF that result in +INF being squared. But since it's not just a single number but an array, it's ok to report a NaN here. Arrays are not numbers.

And while the square root of a negative number is a number mathematically, it's a complex number so it doesn't have a representation in doubles. In doubles, it's also NaN.

The concept works wonderfully. Unless you want to return both a valid number and an error code, you can store them together.

Why?

There are two reasons to store your error codes in NaNs. First is the memory savings. It's not really a problem if you want to get a square root of nine. But if, for instance, you have to store the result of signed distance function in a huge grid for the marching cubes, then suddenly using all the bits you have makes sense.

The second reason is the expected performance gain but it is just the first reason in disguise. You don't make your code faster by reading or writing the error code in NaN. But if you have to store the results somewhere, then storing your codes in NaNs is beneficial. You have more values fitting in a single cache line, and every level of the cache also contains more values, and generally, you have to access less memory to get work done.

I've made this simple benchmark to test how noticeable the gains really are.

enum class ECode : uint64_t {
    OK = 0xFFF0'0000'0000'0001,
    ERROR,
    INPUT_IS_NAN,
    INPUT_IS_INFINITE,
    INPUT_IS_NEGATIVE,
};

union Result_or_code
{
    double result;
    ECode code;
    Result_or_code(double x) {result = x;}
    Result_or_code(ECode c) {code = c;}
    operator double() {return result;}
    operator ECode() {return code;}
};

#define MEASURE(CODE_TO_MEASURE) \
    { \
    auto start = std::chrono::system_clock::now(); \
    CODE_TO_MEASURE \
    auto end = std::chrono::system_clock::now(); \
    std::chrono::duration<double> difference = end - start; \
    std::cout << difference.count(); \
    }
    

Here are eight snippets of code. The first three have error codes as NaNs, then there are a pair of snippets for boost::optional, then there are two more for tuples, and, last but not least, an exception throwing.

Option 1.1. Checking error codes as enum
Result_or_code sqrt_or_not(double x) { if (std::isnan(x)) return ECode::INPUT_IS_NAN; if (std::isinf(x)) return ECode::INPUT_IS_INFINITE; if(x < 0.) return ECode::INPUT_IS_NEGATIVE; return std::sqrt(x); } ... MEASURE( for(double x = -1024.; x <= 1024.; x += 1./65536.) { auto root = sqrt_or_not(x); if(root >= ECode::ERROR) ++errors; else { ++results; total += root; } } );
Time: 0.2 s Return type size: 8 B

Measured on Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz
Compiled with clang version 3.8.0-2ubuntu4
Benchmark code is available on Github.

Storing error codes in NaNs is economical; optional and tuple are almost the same thing; and exceptions are slow. If your goal is to write memory tight code, then the first option is your way to go.

Why not?

There is one reason why you might want to avoid using NaNs as error code carriers: they are already used as error code carriers. Or they aren't. You can't be sure.

IEEE 754 allows signaling NaNs and it's a nice idea conceptually. Some specific NaNs are allowed to interfere with the operations on them. Like when you try to add 2 + NaN, and it doesn't result in yet another NaN but in an exception, you can handle. With that, you're supposed to find more bugs earlier and this is generally a good thing.

The problem is, the exact notation of signaling NaNs, the implementation of traps, or even their very existence, are all unspecified in the standard. This means that if you're aiming at writing cross-platform code, messing with NaNs is probably not the best idea.

Of course, if you're planning on catching all of your error codes right after each and every operation then this is not an issue.

That's what people do, right?

Conclusion

Using NaNs to encode error codes is not sorcery. It's an engineering practice with its benefits and its downsides. Sometimes it is worth doing, sometimes it just isn't. I hope this page could help you make the best decision if you would have to.