Is there a wrapper for floating point numbers in C++20 that would enable me to default the spaceship operator?
Asked Answered
A

2

5

I was watching "Using C++20 three way comparison - Jonathan Müller - Meeting C++ 2019" talk and it mentioned problems with classes that contain floating point members.

Problem comes from the fact that IEEE 754 comparisons involving NaN(s) are weird and do not provide total ordering. Talk gives a way to work around this problems, for example using strong_order or manually ignoring NaN values when implementing <=> (assuming that values are never NaN).

My questions is if there are some library wrappers that would enable me to say that "I promise" that my floats are never NaN or that would do slow but valid comparisons on floats(slower but safer since NaNs are now ordered). My goal is to avoid manual implementation of spaceship by making member float spaceship friendly(so I can default spaceship).

Using example from the talk:

// original class
struct Temperature{
    double value;
};

struct TemperatureNoNan{
    std::a_number<double> value; // I promise value will never be NaN
    // Now spaceship defaulting works
};

struct TemperatureStrongO{
    std::s_ordered<double> value; // I want strong ordering(2 diff NaNs are not the same)
    // Now spaceship defaulting works
};
Auspice answered 22/12, 2019 at 14:43 Comment(3)
I'd assume you'd compare with NaNs by bit_casting to a uint of the same size if both arguments are NaNs, or something.Khotan
@Deduplicator fixed my link(sorry for it being broken) there it says how fp NaNs are handled, but no implementation is providedAuspice
You can default operator<=> for types that contain floats. They simply can't be strongly ordered; they'll have to be weakly ordered. What's the problem? Do you specifically need to have a strong order?Scopoline
A
3

"I promise" that my floats are never NaN

template <std::floating_point T>
struct NeverNaN {
    T val;
    constexpr NeverNaN(T val) : val(val) { }
    constexpr operator T() const { return val; }

    constexpr bool operator==(NeverNaN const&) const = default;

    constexpr std::strong_ordering operator<=>(NeverNaN const& rhs) const {
        auto c = val <=> rhs.val;
        assert(c != std::partial_ordering::unordered);
        return c > 0 ? std::strong_ordering::greater :
                c < 0 ? std::strong_ordering::less :
                std::strong_ordering::equal;
    }
};

Unfortunately, there's no good way to "lift" a comparison category like this. And it doesn't optimize very well at the moment.

that would do slow but valid comparisons on floats(slower but safer since NaNs are now ordered)

This one has specific library support by way of either std::strong_order() or std::weak_order() [cmp.alg] depending on what kind of comparison you want:

template <std::floating_point T>
struct TotallyOrdered {
    T val;
    constexpr TotallyOrdered(T val) : val(val) { }
    constexpr operator T() const { return val; }

    // depends on whether or not you want == NaN to still be false?
    // might need to be: return (*this <=> rhs) == 0; 
    constexpr bool operator==(TotallyOrdered const&) const = default;

    constexpr auto operator<=>(TotallyOrdered const& rhs) const {
        return std::strong_order(val, rhs.val);
        // ... or std::weak_order(val, rhs.val)
    }
};
Afterdinner answered 22/12, 2019 at 16:9 Comment(6)
A bit of oftopic but do you know if this utility in std:: was ever discussed? I ask because you wrote many <=> papers.Auspice
also for if somebody is confused with syntax in second example, recently concepts changed their casing en.cppreference.com/w/cpp/concepts/floating_pointAuspice
@Auspice I don't think this was ever discussed. There are other languages which model partial ordering as like the equivalent of optional<strong_ordering>, at which point lifting it to a strong_ordering is easy (both from the user and optimizer standpoint).Afterdinner
Shame. I am sure WG21 pays a lot of attention to anon comments on S ;) but if this ever gets considered please tell them I would like this feature. As for your example I am not certain how optional<something_that_is_int> is actually optimizer friendly, but I assume that it is still better than code in your answer(from optimizer standpoint, I like your code as far as source code goes), especially if those languages are smart enough to guarantee that optional<strong_ordering> can be compared as int(for example presence is short and strong_ordering is short) and alignment is as for int.Auspice
@Auspice It's super optimizer friendly - optional<T> basically holds a T and a bool so getting the T out of it is simple: godbolt.org/z/_nr5hxAfterdinner
I misunderstood you, I thought we are talking about comparing 2 optionals, and that is obviously wrong, you are just converting single optional of partial order to strong one because in my Q i promised I will not do NaNs so it is safe.Auspice
K
3

There’s nothing in the standard library for this, but it’s trivial to implement:

template<class T,class O=std::weak_ordering>
struct a_number {
  T t;
  O operator<=>(const a_number &rhs) const {
    return t<rhs.t ? O::less : t==rhs.t ? O::equivalent : O::greater;
  }
};

template<class T>
struct s_ordered {
  T t;
  auto operator<=>(const s_number &rhs) const {
    return std::strong_order(t,rhs.t);
  }
};

…with whatever other conversion operators or other conveniences desired.

Kymry answered 22/12, 2019 at 16:8 Comment(0)
A
3

"I promise" that my floats are never NaN

template <std::floating_point T>
struct NeverNaN {
    T val;
    constexpr NeverNaN(T val) : val(val) { }
    constexpr operator T() const { return val; }

    constexpr bool operator==(NeverNaN const&) const = default;

    constexpr std::strong_ordering operator<=>(NeverNaN const& rhs) const {
        auto c = val <=> rhs.val;
        assert(c != std::partial_ordering::unordered);
        return c > 0 ? std::strong_ordering::greater :
                c < 0 ? std::strong_ordering::less :
                std::strong_ordering::equal;
    }
};

Unfortunately, there's no good way to "lift" a comparison category like this. And it doesn't optimize very well at the moment.

that would do slow but valid comparisons on floats(slower but safer since NaNs are now ordered)

This one has specific library support by way of either std::strong_order() or std::weak_order() [cmp.alg] depending on what kind of comparison you want:

template <std::floating_point T>
struct TotallyOrdered {
    T val;
    constexpr TotallyOrdered(T val) : val(val) { }
    constexpr operator T() const { return val; }

    // depends on whether or not you want == NaN to still be false?
    // might need to be: return (*this <=> rhs) == 0; 
    constexpr bool operator==(TotallyOrdered const&) const = default;

    constexpr auto operator<=>(TotallyOrdered const& rhs) const {
        return std::strong_order(val, rhs.val);
        // ... or std::weak_order(val, rhs.val)
    }
};
Afterdinner answered 22/12, 2019 at 16:9 Comment(6)
A bit of oftopic but do you know if this utility in std:: was ever discussed? I ask because you wrote many <=> papers.Auspice
also for if somebody is confused with syntax in second example, recently concepts changed their casing en.cppreference.com/w/cpp/concepts/floating_pointAuspice
@Auspice I don't think this was ever discussed. There are other languages which model partial ordering as like the equivalent of optional<strong_ordering>, at which point lifting it to a strong_ordering is easy (both from the user and optimizer standpoint).Afterdinner
Shame. I am sure WG21 pays a lot of attention to anon comments on S ;) but if this ever gets considered please tell them I would like this feature. As for your example I am not certain how optional<something_that_is_int> is actually optimizer friendly, but I assume that it is still better than code in your answer(from optimizer standpoint, I like your code as far as source code goes), especially if those languages are smart enough to guarantee that optional<strong_ordering> can be compared as int(for example presence is short and strong_ordering is short) and alignment is as for int.Auspice
@Auspice It's super optimizer friendly - optional<T> basically holds a T and a bool so getting the T out of it is simple: godbolt.org/z/_nr5hxAfterdinner
I misunderstood you, I thought we are talking about comparing 2 optionals, and that is obviously wrong, you are just converting single optional of partial order to strong one because in my Q i promised I will not do NaNs so it is safe.Auspice

© 2022 - 2024 — McMap. All rights reserved.