避免代码重复的最佳方法,定义比较运算符"<,<=,>,>=,==,!=",但要考虑NaN?

Best way to avoid code duplication defining comparison operators `<, <=, >, >=, ==, !=`, but taking into account NaNs?

本文关键字:gt lt NaN 代码 比较 定义 最佳 运算符 方法      更新时间:2023-10-16

I数学,x <= y等价于!(x > y)。这对于浮点运算是正确的,在大多数情况下,但并非总是如此。当xy为NaN时,x <= y 不等于!(x > y),因为比较NaN和任何东西总是返回false。但是,x <= y <=> !(x > y)在大多数情况下是正确的。

现在,假设我正在编写一个包含浮点值的类,并且希望为该类定义比较操作符。为了明确起见,假设我正在编写一个高精度浮点数,它在内部使用一个或多个double值来存储高精度数。从数学上讲,该类的x < y定义已经定义了所有其他操作符(如果我与比较操作符的通常语义一致的话)。但是NaN打破了这种数学上的精确。也许我不得不把这些算子分开写,只是为了考虑到nan。但是有没有更好的办法呢?我的问题是:如何尽可能避免代码重复,同时仍然尊重NaN的行为?

相关:http://www.boost.org/doc/libs/1_59_0/libs/utility/operators.htm。boost/operators如何解决这个问题?

注意:我标记这个问题c++,因为这是我所理解的。请用那种语言写例子

我个人会使用与这个答案类似的技术,该技术基于operator<()定义比较函数,产生严格的弱顺序。对于具有空值的类型,其比较总是产生false,操作将根据operator<()定义,为所有非空值提供严格的弱顺序和is_null()测试。

例如,代码可以是这样的:
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);
    }
}

可以这样使用:

#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(); }

相同主题的变体可以是一个比较函数的实现,该比较函数由比较函数参数化,并负责适当地为比较函数提供参数。例如:

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

我可以想象有多个这样的操作生成名称空间来支持对常见情况的合适选择。另一个选项可能是与浮点数不同的排序,例如,将空值视为最小值或最大值。由于有些人使用NaN装箱,因此提供不同NaN值的顺序并将NaN值安排在合适的位置可能是合理的。例如,使用底层的位表示提供浮点值的总顺序,这可能适用于将对象用作有序容器中的键,尽管该顺序可能与operator<()创建的顺序不同。

定义运算符<你需要处理NaN的案子。出于排序和比较的目的,如果您将NaN视为小于任何非NaN,则可以这样做:>

bool operator<(double l, double r) {
    if (isnan(l)) {
        if (isnan(r)) return false; // NaN == NaN
        return true;        // NaN < rational
    }
    return l < r;       // if r is NaN will return false, which is how we've defined it
}

其他操作符用operator<并且不需要手动编写代码。>