1. 程式人生 > 其它 >C++20 三向比較

C++20 三向比較

C++20引入了<=>運算子,極大的簡化了比較操作。

老兵

作為一名在戰場上出生入死多年的老兵,對於手中的武器C++,我有充分的理由相信自己已經對她身上的每一寸肌膚都瞭如指掌,直到有一天,我被下面的程式碼嚇了一跳:

        struct Num
        {
            int a;
            long b;

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

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

這是個啥?<=>是什麼?這還是你最熟悉的那個她嗎?於是,我趕緊開啟最新的C++標準手冊。

很快,我就發現,這玩意真的很扯淡,增加了一個運算子,就是為了簡化一下關係運算符的實現?

我快速的打開了自己視若性命的《C++保命手冊》,快速翻到實現關係運算符的部分:

        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); }

我長舒了一口氣,這才是C++嘛,這才是自定義型別關係運算符功能的標準實現嘛。其中我最引以為傲,不斷向新兵們炫耀的兩大祕籍是:

1、只需要實現operator<和operator==這2個關係運算符,剩下的4個關係運算符可以藉助於std::rel_ops空間中的模板自動推匯出來

2、實現六種關係運算符會使你的程式碼看起來漂亮很多(因為運算子過載),但是那些從C語言時代過來的更老練的老兵們其實更喜歡實現compare,真的是一句話搞定所有問題。

當然,上面的程式碼還可以精簡一下,可以通過呼叫compare來實現那2個關係運算符。

新兵

新兵對老兵的所謂祕籍是不屑一顧的,他們直接搬出來下面的程式碼:

        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);
        }

對於int型別來說,<=>運算子可以返回3種結果,從理論上說,新標準對比較這個功能做了更為嚴謹的劃分,int型別是可以強順序比較的(std::strong_ordering),兩個量一旦相等同時也就意味著兩個量可以互換。

至於老兵們喜歡的compare,通過引入std::strong_ordering和常量0(或nullptr)的比較,新的語法可以很好的模擬compare的語法。

當然了,有std::strong_ordering就意味著還有非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);
        }

因為浮點數有精度問題,即使相等也不一定可以互換,所以它是弱順序比較。

還有其它型別的比較,不過都大同小異,就不多說了。

對於新兵來說,真正關鍵的是,<=>運算子的預設實現是全自動的:

  • 會替你比較自定義型別中每一個成員變數
  • 會替你處理基類、子物件等這些細節
  • 會替你自動完成關係運算符的實現
        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);
        }

你看,我們的Num類幾乎啥也沒幹,就是招呼了一下<=>的預設實現,它就替我們搞定了所有的事情。

“會替你自動完成關係運算符的實現”,這句話有些不太嚴謹,因為後來有人發現<=>的預設實現在自定義型別帶vector成員變數時,效能會有些問題。所以新的C++20標準規定:

<=>的預設實現不再自動生成operator==和operator!=這2種關係運算符。

有了<=>後,編譯器甚至不再推薦std::rel_ops的使用了,直接會給出警告(對老兵來說真是殘忍啊)。如果非要用這個技巧,那就必須定義SILENCE_CXX20_REL_OPS_DEPRECATION_WARNING

搗蛋鬼

老兵們喜歡擺弄自己心愛的手動武器,新兵們則喜歡隨便抓過來一隻最新的自動武器就衝向靶場。

“哎,這些新兵蛋子真是越來越墮落了,未來打仗估計都得給他們每個人配一個輔助機器人。”

老兵們太爽<=>的預設實現揹著自己搞出來一堆事情,這方便是方便了,卻總會讓人惴惴不安。很快,一個老兵中的搗蛋鬼就搞出了下面的程式碼:

        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);                // 使用者自定義
            Assert::IsTrue(b > a);
            Assert::IsTrue(a <= b);
            Assert::IsTrue(b >= a);
            Assert::IsTrue(a == NumEx(2, 2));    // 使用者自定義
            Assert::IsTrue(a != b);
        }

<=>的預設實現再厲害,你還能不讓我自定義關係運算符了?於是這裡就產生了衝突,到底是用<=>自動生成的關係運算符實現還是用自定義關係運算符的實現呢?

答案當然是後者,無論何時何地,在C++語言中,程式設計師自定義的優先順序最高。

這裡,我們故意設計了一個非常另類的比較規則,以區別<=>的預設實現,我們發現:

1、operator<採用了程式設計師自定義的實現,而operator>,operator<=,operator>=這3個卻採用了<=>的預設實現

2、operator==和operator!=採用了程式設計師自定義的實現,<=>的預設實現果然不再自動生成這2個關係運算符

道理很簡單,但是這場面實在是尷尬,新兵們稍有不察都得掉坑裡。看來還是得約定一個編碼規則:

不允許自定義<=>和自定義關係運算符混用