Ответ 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<()
.