I'm gradually writing an arbitrary-precision arithmetic library for C++. I've decided it could be useful to have the default constructor produce an object of indeterminate value, rather than relying upon {} = 0
, to make my code more explicit.
I'm not yet sure whether I want operations like division by zero or square root of negatives to return NaN —currently I throw an exception for zero division, but it could be used for these later too.
I also think that for my library, it is more useful for NaN to compare equal with itself —I know this is the opposite behaviour to floating point, but I'm not intending to ever implement fp behaviour with my library and from reading up the history of why float NaN doesn't equal itself, it seems this was done mostly for technical limitations at the time rather than reasoning.
Here's the implementation I have come up with, using an empty tag-like struct to represent NaN, so it can be used with multiple different arbitrary-precision types easily:
#include <cstdint>
#include <compare>
static constexpr struct {} NaN = {};
class Number {
public:
constexpr Number() : Number(NaN) {}
constexpr Number(decltype(NaN)) : _is_nan(true) {}
constexpr Number(std::intmax_t number) : _number(number) {}
constexpr std::partial_ordering operator<=>(const Number& rhs) const {
// NaNs are stritly unordered --unless when checking for NaN == NaN
if (_is_nan or rhs._is_nan) { return std::partial_ordering::unordered; }
// otherwise compare values by default
return _number <=> rhs._number; // NOTE: implicitly converts strong to partial ordering
}
// for our equality operator, we actually *DO* want the default, as we want NaN == NaN
constexpr bool operator==(const Number& rhs) const = default;
private:
std::intmax_t _number = 0;
bool _is_nan = false;
};
int main() {
static_assert(Number(0) == Number(0));
static_assert(Number() == Number());
static_assert(Number() == NaN);
static_assert(Number() == Number(NaN));
static_assert(Number(45) <= Number(67));
static_assert(Number(37) >= 21);
static_assert(not (Number(32) > NaN));
static_assert(not (Number() > Number()));
static_assert(not (Number() <= NaN));
static_assert(not (Number() < NaN));
}
Notes:
- Obviously this class isn't arbitrary precision. That code is too long and it's not relevant to this question, which is about API design. I have included only a tiny stub implementation of the value part of the class here, to focus on the implementation of NaNs.
- In practice, I will probably represent NaN internally as an object with no digits in its digit array, and there will be no
_is_nan
flag.- Yes,
constexpr
is deliberate and yes, the existing current working version of the library does provide a fully workingconstexpr
arbitrary-precision type. You can use it at compile-time but only if it's deallocated before the constant expression it is used in goes out of scope. This limits its utility but it is still a highly useful thing to have.- All code is C++20
What do you think of my API design? I pondered whether I should just use a static constant NAN
in the class and use that to get NAN, but this approach feels more user-friendly because one constant can be used with all of the arbitrary-precision classes I'm going to provide.
operator-overloading
but it has changed tooverloading
automatically and this happens every time I try and change it back. \$\endgroup\$NaN
" comes with a lot of 754 baggage at this point. Users of your library will believe they know what the term means, and they will be astonished. Consider adopting a similar but different name for the concept you wish to convey to an audience of engineers who have already used numeric libraries. Also, "NaNs are strictly unordered" seems like trouble for <=, <, ==. Show us real caller code! \$\endgroup\$