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, intdenominator_ = 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個關係運算符
道理很簡單,但是這場面實在是尷尬,新兵們稍有不察都得掉坑裡。看來還是得約定一個編碼規則:
不允許自定義<=>和自定義關係運算符混用