Лучший способ избежать дублирования кода, определяющего операторы сравнения `<, <=,>,> =, ==,! =`, Но принимая во внимание NaNs?

I математика, x <= y эквивалентна !(x > y). Это справедливо для арифметики с плавающей запятой, в большинстве случаев, но не всегда. Если x или y - NaN, x <= y не эквивалентно !(x > y), так как сравнение a NaN ко всему всегда возвращает false. Но все же, x <= y <=> !(x > y) истинно больше всего времени.

Теперь предположим, что я пишу класс, содержащий значения с плавающей запятой, и я хочу определить операторы сравнения для этого класса. Предположим, что я пишу высокоточное число с плавающей запятой, которое использует одно или несколько значений double для хранения высокоточного числа. Математически определение x < y для этого класса уже определяет все остальные операторы (если я согласен с обычной семантикой операторов сравнения). Но NaN нарушить эту математическую тонкость. Поэтому, может быть, мне приходится писать многие из этих операторов отдельно, просто чтобы учесть NaNs. Но есть ли лучший способ? Мой вопрос: как я могу избежать дублирования кода как можно больше и по-прежнему уважать поведение NaN?

Связано: http://www.boost.org/doc/libs/1_59_0/libs/utility/operators.htm. Как оптимизация/операторы разрешают эту проблему?

Примечание. Я отметил этот вопрос c++, потому что это то, что я понимаю. Пожалуйста, напишите примеры на этом языке.

Ответ 1

Лично я использовал бы подобный метод, как в этот ответ, который определяет функцию сравнения на основе 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<().

Ответ 2

Когда вы определяете свой оператор < вам нужно обработать дело 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
}

Другие операторы определяются в терминах оператора < и не требуют никакого написанного вручную кода.