C++20 three-way comparison

 

veteran

As a veteran who has lived and died on the battlefield for many years, I have every reason to believe that I already know every inch of her skin about the weapon C + +. Until one day, I was shocked by the following code:

        struct Num
        {
            int a;
            long b;

            Num(int a_ = 0, long b_ = 0)
                :a(a_), b(b_)
            {
            }

            auto operator <=> (const Num&) const = default;
        };

What is this? < = > What is it? Is this still the one you know best? So I quickly opened the latest C + + standard manual.

Soon, I found that this thing is really bullshit. An operator was added to simplify the implementation of relational operators?

I quickly opened my life saving manual for C + + and quickly turned to the part of implementing relational operators:

        struct Rational
        {
            int numeratorand;
            int denominator;

            Rational(int numeratorand_ = 0, int denominator_ = 1)
                :numeratorand(numeratorand_), denominator(denominator_)
            {

            }

            bool operator<(const Rational& other) const
            {
                return numeratorand * other.denominator -
                    other.numeratorand * denominator < 0;
            }

            bool operator==(const Rational& other) const
            {
                return numeratorand * other.denominator -
                    other.numeratorand * denominator == 0;
            }

            inline int compare(const Rational& other) const
            {
                return numeratorand * other.denominator -
                    other.numeratorand * denominator;
            }
        };

        TEST_METHOD(TestRational)
        {
            Rational a(1, 2);
            Rational b(2, 3);
            Assert::IsTrue(a < b);
            Assert::IsTrue(b > a);
            Assert::IsTrue(a <= b);
            Assert::IsTrue(b >= a);
            Assert::IsTrue(a == a);
            Assert::IsTrue(a != b);

            Assert::IsTrue(a.compare(b) < 0);
            Assert::IsTrue(a.compare(a) == 0);
            Assert::IsTrue(b.compare(a) > 0);
        }

I breathe a sigh of relief. This is C + +, and this is the standard implementation of the function of custom type relational operators. Among them, the two secrets I am most proud of and constantly show off to recruits are:

1. Only the two relational operators of operator < and operator = = need to be implemented, and the remaining four relational operators can be implemented with the help of STD:: rel_ Templates in OPS space are automatically derived

2. Implementing Six relational operators will make your code look much more beautiful (because of operator overloading), but those more experienced veterans from the C language era actually prefer to implement compare, which really solves all problems in one sentence.

Of course, the above code can also be simplified. You can call compare to implement the two relational operators.

recruit

Recruits despise the so-called secrets of veterans. They directly move out the following code:

        TEST_METHOD(TestInt)
        {
            int a = 1;
            int b = 2;

            Assert::IsTrue(a <=> b == std::strong_ordering::less);
            Assert::IsTrue(a <=> a == std::strong_ordering::equal);
            Assert::IsTrue(b <=> a == std::strong_ordering::greater);

            Assert::IsTrue((a <=> b) < 0);
            Assert::IsTrue((a <=> a) == 0);
            Assert::IsTrue((b <=> a) > 0);

            Assert::IsTrue((a <=> b) < nullptr);
            Assert::IsTrue((a <=> a) == nullptr);
            Assert::IsTrue((b <=> a) > nullptr);
        }

For int types, the < = > operator can return three results. Theoretically, the new standard makes a more rigorous division of the function of comparison. Int types can be compared in strong order (std::strong_ordering). Once the two quantities are equal, it means that the two quantities can be interchanged.

As for compare, which veterans like, by introducing STD:: strong_ Compared with the constant 0 (or nullptr), the new syntax can well simulate the syntax of compare.

Of course, there is std::strong_ordering means that there are non std::strong_ordering:

        TEST_METHOD(TestDouble)
        {
            double a = 1;
            double b = 2;

            bool is = 0.0 == -0.0;

            Assert::IsTrue(a <=> b == std::weak_ordering::less);
            Assert::IsTrue(a <=> a == std::weak_ordering::equivalent);
            Assert::IsTrue(b <=> a == std::weak_ordering::greater);

            Assert::IsTrue((a <=> b) < 0);
            Assert::IsTrue((a <=> a) == 0);
            Assert::IsTrue((b <=> a) > 0);

            Assert::IsTrue((a <=> b) < nullptr);
            Assert::IsTrue((a <=> a) == nullptr);
            Assert::IsTrue((b <=> a) > nullptr);
        }

Because floating point numbers have precision problems, even if they are equal, they may not be interchangeable, so they are weak order comparison.

There are other types of comparisons, but they are similar, so I won't say more.

For recruits, the real key is that the default implementation of the < = > operator is fully automatic:

  • It will compare each member variable in the custom type for you
  • It will handle the details of base classes and sub objects for you
  • Will automatically complete the implementation of relational operators for you
        struct Num
        {
            int a;
            long b;

            Num(int a_ = 0, long b_ = 0)
                :a(a_), b(b_)
            {
            }

            auto operator <=> (const Num&) const = default;
        };

        TEST_METHOD(TestNum)
        {
            Num a(1, 1);
            Num b(1, 2);

            Assert::IsTrue(a == a);
            Assert::IsTrue(a != b);

            Assert::IsTrue(a <=> b == std::weak_ordering::less);
            Assert::IsTrue(a <=> a == std::weak_ordering::equivalent);
            Assert::IsTrue(b <=> a == std::weak_ordering::greater);

            Assert::IsTrue((a <=> b) < 0);
            Assert::IsTrue((a <=> a) == 0);
            Assert::IsTrue((b <=> a) > 0);

            Assert::IsTrue(a < b);
            Assert::IsTrue(b > a);
            Assert::IsTrue(a <= b);
            Assert::IsTrue(b >= a);
            Assert::IsTrue(a == a);
            Assert::IsTrue(a != b);
        }

You see, our Num class did almost nothing. It just greeted the default implementation of < = > and it took care of everything for us.

The sentence "it will automatically complete the implementation of relational operators for you" is somewhat less rigorous, because it was later found that the performance of the default implementation of < = > will have some problems when the user-defined type has vector member variables. Therefore, the new C++20 standard stipulates:

The default implementation of < = > no longer automatically generates operator = = and operator= These two relational operators.

With < = >, the compiler does not even recommend STD:: rel_ The use of OPS will give a warning directly (it's cruel for veterans). If you have to use this technique, you must define SILENCE_CXX20_REL_OPS_DEPRECATION_WARNING

Troublemaker

Veterans like to play with their favorite manual weapons, while recruits like to grab a new automatic weapon and rush to the shooting range.

"Hey, these recruits are getting worse and worse. It is estimated that each of them will have to be equipped with an auxiliary robot in the future war."

Veterans are so happy that the default implementation of < = > makes a lot of things behind their backs. It's convenient, but it always makes people nervous. Soon, a troublemaker among veterans came up with the following code:

        struct NumEx
        {
            int a;
            long b;

            NumEx(int a_ = 0, long b_ = 0)
                :a(a_), b(b_)
            {
            }

            bool operator<(const NumEx& other) const
            {
                return a + b < other.a + other.b;
            }

            bool operator==(const NumEx& other) const
            {
                return a + b == other.a + other.b;
            }

            std::strong_ordering operator <=> (const NumEx&) const = default;
        };

        TEST_METHOD(TestNumEx)
        {
            NumEx a(1, 3);
            NumEx b(2, 1);

            Assert::IsTrue(a == a);
            Assert::IsTrue(a != b);

            Assert::IsTrue(a <=> b == std::strong_ordering::less);
            Assert::IsTrue(a <=> a == std::strong_ordering::equal);
            Assert::IsTrue(b <=> a == std::strong_ordering::greater);

            Assert::IsTrue((a <=> b) < 0);
            Assert::IsTrue((a <=> a) == 0);
            Assert::IsTrue((b <=> a) > 0);

            Assert::IsFalse(a < b);                // User defined
            Assert::IsTrue(b > a);
            Assert::IsTrue(a <= b);
            Assert::IsTrue(b >= a);
            Assert::IsTrue(a == NumEx(2, 2));    // User defined
            Assert::IsTrue(a != b);
        }

No matter how powerful the default implementation of < = > is, can you stop me from customizing relational operators? Therefore, there is a conflict here. Is it implemented by < = > automatically generated relational operators or by user-defined relational operators?

Of course, the answer is the latter. Whenever and wherever, programmer customization has the highest priority in C + + language.

Here, we deliberately designed a very alternative comparison rule to distinguish the default implementation of < = >. We found that:

1. Operator < < adopts the implementation defined by the programmer, while operator >, operator < =, operator > = adopts the default implementation of < = >

2. Operator = = and operator= The programmer defined implementation is adopted, and the default implementation of < = > does not automatically generate these two relational operators

The reason is very simple, but the scene is really embarrassing. The recruits have to fall into the pit if they don't notice it. It seems that we still have to agree on a coding rule:

Mixing custom < = > and custom relational operators is not allowed

Added by Z3RatuL on Thu, 28 Oct 2021 13:41:03 +0300