I mathematics, x <= y
is equivalent to !(x > y)
. This is true for floating-point arithmetic, in most cases, but not always. When x
or y
is NaN, x <= y
is not equivalent to !(x > y)
, because comparing a NaN
to anything always returns false
. But still, x <= y <=> !(x > y)
is true most of the time.
Now, suppose I am writing a class that contains floating-point values, and I want to define comparison operators for this class. For definiteness, suppose I am writing a high-precision floating-point number, which uses one or more double
values internally to store the high-precision number. Mathematically, the definition of x < y
for this class already defines all the other operators (if I am being consistent with the usual semantics of the comparison operators). But NaN
s break this mathematical nicety. So maybe I am forced to write many of these operators separately, just to take into account NaNs. But is there a better way? My question is: How can I avoid code duplication as much as possible and still respect the behavior of NaN
?
Related: http://www.boost.org/doc/libs/1_59_0/libs/utility/operators.htm. How does boost/operators resolve this issue?
Note: I tagged this question c++
because that's what I understand. Please write examples in that language.
Explanation: The operator =! Is not the comparison operator.
Comparison operators — operators that compare values and return true or false . The operators include: > , < , >= , <= , === , and !== . Logical operators — operators that combine multiple boolean expressions or values and provide a single boolean output.
Comparison operators in C++ are the ones that are there to compare two values with each other such as “==”, “!= ”, “>”, “<”, “>=”, and “<=”. This article will share the methods of overloading all six of these comparison operators in C++ in Ubuntu 20.04.
Personally I would use a similar technique as in this answer which defines comparison function based on operator<()
yielding a strict weak order. For types with a null value which is meant to have comparisons always yield false
the operations would be defined in terms of operator<()
providing a strict weak order on all non-null value and an is_null()
test.
For example, the code could look like this:
namespace nullable_relational {
struct tag {};
template <typename T>
bool non_null(T const& lhs, T const& rhs) {
return !is_null(lhs) && !is_null(rhs);
}
template <typename T>
bool operator== (T const& lhs, T const& rhs) {
return non_null(lhs, rhs) && !(rhs < lhs) && !(lhs < rhs);
}
template <typename T>
bool operator!= (T const& lhs, T const& rhs) {
return non_null(lhs, rhs) || !(lhs == rhs);
}
template <typename T>
bool operator> (T const& lhs, T const& rhs) {
return non_null(lhs, rhs) && rhs < lhs;
}
template <typename T>
bool operator<= (T const& lhs, T const& rhs) {
return non_null(lhs, rhs) && !(rhs < lhs);
}
template <typename T>
bool operator>= (T const& lhs, T const& rhs) {
return non_null(lhs, rhs) && !(lhs < rhs);
}
}
It would be use like this:
#include <cmath>
class foo
: private nullable_relational::tag {
double value;
public:
foo(double value): value(value) {}
bool is_null() const { return std::isnan(this->value); }
bool operator< (foo const& other) const { return this->value < other.value; }
};
bool is_null(foo const& value) { return value.is_null(); }
A variation of the same theme could be an implementation in terms of one compare function which is parameterized by the comparison function and which is responsible to appropriately feed the comparison function with parameters. For example:
namespace compare_relational {
struct tag {};
template <typename T>
bool operator== (T const& lhs, T const& rhs) {
return compare(lhs, rhs, [](auto&& lhs, auto&& rhs){ return lhs == rhs; });
}
template <typename T>
bool operator!= (T const& lhs, T const& rhs) {
return compare(lhs, rhs, [](auto&& lhs, auto&& rhs){ return lhs != rhs; });
}
template <typename T>
bool operator< (T const& lhs, T const& rhs) {
return compare(lhs, rhs, [](auto&& lhs, auto&& rhs){ return lhs < rhs; });
}
template <typename T>
bool operator> (T const& lhs, T const& rhs) {
return compare(lhs, rhs, [](auto&& lhs, auto&& rhs){ return lhs > rhs; });
}
template <typename T>
bool operator<= (T const& lhs, T const& rhs) {
return compare(lhs, rhs, [](auto&& lhs, auto&& rhs){ return lhs <= rhs; });
}
template <typename T>
bool operator>= (T const& lhs, T const& rhs) {
return compare(lhs, rhs, [](auto&& lhs, auto&& rhs){ return lhs >= rhs; });
}
}
class foo
: private compare_relational::tag {
double value;
public:
foo(double value): value(value) {}
template <typename Compare>
friend bool compare(foo const& f0, foo const& f1, Compare&& predicate) {
return predicate(f0.value, f1.value);
}
};
I could imagine having multiple of these operation-generating namespaces to support a suitable choice for common situations. Another option could be a different ordering than floating points and, e.g., consider a null value the smallest or the largest value. Since some people use NaN-boxing it may even be reasonable to provide an order on different NaN values and arrange the NaN values at suitable places. For example, using the underlying bit representation provide a total order of floating point values which may be suitable for using the objects as a key in an ordered container although the order might be different from the order created by operator<()
.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With