Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does spaceship allow mixed comparisons (different template instantiations) with nonsense results?

Tags:

EDIT: This has nothing to do with spaceship. It is just that the use of spaceship obfuscated the real issue in my code (see answer for details).

I was surprised by the output of this program: (If you like puzzles feel free to open the Godbolt link and try to spot the cause yourself)

#include <cstdint>
#include <cassert>
#include <compare>
#include <cmath>
#include <iostream>
#include <limits>

template<typename T>
struct TotallyOrdered
{
    T val;
    constexpr TotallyOrdered(T val) :
        val(val) {}
    constexpr operator T() const { return val; }
    constexpr std::strong_ordering operator<=>(TotallyOrdered const& other) const
    {
        if (std::isnan(val) && std::isnan(other.val))
        {
            return std::strong_ordering::equal;
        }
        if (std::isnan(val))
        {
            return std::strong_ordering::less;
        }
        if (std::isnan(other.val))
        {
            return std::strong_ordering::greater;
        }
        if (val < other.val)
        {
            return std::strong_ordering::less;
        }
        else if (val == other.val)
        {
            return std::strong_ordering::equal;
        }
        else
        {
            assert(val > other.val);
            return std::strong_ordering::greater;
        }
    }
};



int main()
{
    const auto qNan = std::numeric_limits<float>::quiet_NaN();
    std::cout << std::boolalpha;
    std::cout << ((TotallyOrdered{qNan} <=> TotallyOrdered{1234.567}) ==  std::strong_ordering::less) << std::endl;
    std::cout << ((TotallyOrdered{qNan} <=> TotallyOrdered{1234.567}) ==  std::strong_ordering::equal) << std::endl;
    std::cout << ((TotallyOrdered{qNan} <=> TotallyOrdered{1234.567}) ==  std::strong_ordering::equivalent) << std::endl;
    std::cout << ((TotallyOrdered{qNan} <=> TotallyOrdered{1234.567}) ==  std::strong_ordering::greater) << std::endl;
}

output:

false
false
false
false

After a bit of blaming Godbolt caching... I have figured out that the problem is that I was comparing TotallyOrdered<float> and TotallyOrdered<double> (adding f after 1234.567 gives expected output). My questions are:

  • Why is this allowed? (Not asking if this is standard behavior; it is, but curious about design intention.)
  • Why do comparisons give none of the "enums" in strong_ordering? It looks like I get a partial order when I do a mixed comparison although I have defined only strong_order <=>.
  • How can I force that only "exact +-cvref" comparisons (that give a std::strong_ordering result) compile, preventing comparisons that give std::partial_ordering?
like image 451
NoSenseEtAl Avatar asked Feb 26 '21 10:02

NoSenseEtAl


1 Answers

It's allowed because your conversion operator to T is not explicit. This allows both sides of the comparison to undergo a user-defined conversion to their respective T. So you end up with a float and a double. And then those can both be converted to double and a comparison can happen. But that comparison returns an std::partial_ordering, not an std::strong_ordering.

Note that std::strong_ordering can be compared to a bool, which is why your code compiles in the first place. Although cppreference.com does note that:

The behavior of a program that attempts to compare a strong_ordering with anything other than the integer literal ​0​ is undefined.

I'm not a 100% sure whether your program is displaying undefined behavior, or whether there's some more conversion/promotion "magic" going on.

Either way, if you change your conversion operator to be explicit, the code won't compile anymore. Which I guess is what you actually want?

like image 90
AVH Avatar answered Sep 30 '22 17:09

AVH