摘要訊息 : 一個更加完善的比較方案.

0. 前言

0.1 繁瑣的比較程式碼

C++ 中有六個用於比較的運算子, 對於一個有序的自訂型別來說, 我們必須實作這六個比較運算子 :

class int_holder {
    int a;
public:
    explicit int_holder(int);
    explicit operator int();
    // other functions...
public:
    bool operator==(const int_holder &);
    bool operator!=(const int_holder &);
    bool operator<(const int_holder &);
    bool operator<=(const int_holder &);
    bool operator>(const int_holder &);
    bool operator>=(const int_holder &);
};

int_holder 是用於儲存一個 int 型別的值且行為類似於 int 型別的類別, 但是我們不希望它向其它型別發生隱含型別轉化, 所以接受 int 型別引數的建構子和向 int 轉型的多載轉型運算子都被 explicit 標識. 為了使得 int_holder 的行為盡量類似於內建的 int 型別, 我們肯定還需要增加不少友誼比較運算子, 使得 int_holder 類別可以和 bool, char, unsigned char, signed char, short, unsigned short, int, unsigned int, long, unsigned long, long long, unsigned long long, wchar_t, char8_t (C++ 20), char16_tchar32_t 這幾個內建的整型型別進行比較. 除此之外, 還需要增加和 float, doublelong double 這三個內建的浮點數型別的比較 (甚至 __int128_t__uint128_t). 我們希望 int_holder {} == 1 能夠成立, 也希望 1 == int_holder {} 能夠成立, 這樣一算, 我們至少還需要實作 228 個函式. 即時我們只要求 intint_holder 之間的比較, 也還需要實作 12 個函式.

0.2 瑕疵的浮點數比較

直到 C++ 17 為止, 可以說 C++ 針對浮點數的比較是有瑕疵的. 根據 IEEE 754 二進位浮點數算術標準, 存在一個無法比較的浮點數 NaN (Not a Number), 表示無效的數字, 一般產生於錯誤的運算式中. 例如對於 \sqrt {-1}\ln {(-1)}, C++ 就會回傳 NaN (在 C++ 中被定義為 NAN, 它是一個由 C++ 標準樣板程式庫定義的巨集). NaN 除了不能和任何浮點數進行比較之外, 也不能和自身進行比較 :

#include <iostream>
#include <cmath>

using namespace std;
int main(int argc, char *argv[]) {
    auto a {sqrt(-1)};
    cout << boolalpha;
    cout << (a > 0.0) << endl;      // 輸出結果 : false
    cout << (a < 0.0) << endl;      // 輸出結果 : false
    cout << (a == 0.0) << endl;     // 輸出結果 : false
    cout << (sqrt(-1) == sqrt(-1)) << endl;     // 輸出結果 : false
    cout << (log10(-1) > log10(-1)) << endl;        // 輸出結果 : false
}

這就導致在 IEEE 754 標準之下, 浮點數並不是完全有序的, 因為浮點數集合中存在 NaN, 它無法和其它集合內的元素進行比較. 於是, 上面回傳 false 的結果是錯誤的, 或者說是有瑕疵的. 正確的做法應該是回傳一個標識無序的結果, 擲出例外情況或者直接擲出編碼錯誤. 實際上, 由於 NaN 也是浮點數型別, 任何情況都在編碼期下進行檢查是不可能的.

0.3 用什麼來表示無序的結果?

儘管 C++ 標準沒有規定 bool 型別的大小, 即 sizeof(bool) 的值, 但是編碼器普遍用八個位元, 也就是一個位元組, 來表示 bool 型別. 然而, bool 只有兩個值 : truefalse. 如果用 C++ 實作了一個家族族譜, 用 "A > B" 表示 AB 的父母, "A < B" 表示 AB 的子女, "A == B" 來表示 AB 是同一個人, 那麼該用什麼表示 AB 沒有直系關係呢? 這就回到了浮點數 NaN 的比較問題. 顯然, 我們需要判斷 (A > B == false) and (A < B == false) and ((A == B) == false). 如果這個表達式為 true, 那麼 AB 沒有直系關係. 這種表達式非常繁瑣. 一個比較好的方法是另外實作一些類別 :

struct true_result {
    constexpr operator bool() noexcept {
        return true;
    }
};
struct false_result {
    constexpr operator bool() noexcept {
        return false;
    }
};
struct unordered {};

這樣的話, 當 AB 沒有直系關係卻進行比較的時候, 就可以直接擲出編碼錯誤. 想法是不錯的, 但是現實是殘酷的 : 當 AB 編碼期不可知的時候, 回傳哪一個型別呢? 難道要讓 true_result, false_resultunordered 相互可以發生隱含型別轉化? 這還不如直接使用表達式 (A > B == false) and (A < B == false) and ((A == B) == false) 來判斷結果.

其實這個實例和浮點數比較是類似的, 而對於 NaN 的比較回傳 false 是存在瑕疵的, 而在這個問題中回傳 false 並不存在瑕疵, 是正確的.

0.4 繼承自 C 語言的陣列特性

由於 C++ 需要和 C 保持相容性, 因此 C++ 維持了 C 語言中無法對陣列進行比較的特性. 當我們需要比較兩個陣列的時候, 我們需要編寫額外的函式, 而不是採用 C++ 的運算子多載, 因為對內建型別的多載是不允許的. 這同樣也導致了我們無法使用 array1 == array2 這樣的語法來比較兩個陣列的相等性. 這至少不太優雅, 或者說這不太 C++.

0.5 三路比較運算子 (宇宙飛船運算子) operator<=>

為了解決上面提到的存在於 C++ 中關於比較的一些問題, C++ 20 引入了一個全新的比較運算子 : 三路比較 (three-way comparison) 運算子 <=>. 由於這個符號和宇宙飛船的樣子很像, 所以我們也叫它宇宙飛船運算子 (有一些程式設計語言就叫 <=> 為宇宙飛船運算子). 由於這個運算子是左右對稱的, 我們可以利用對稱性解決第 0.1 節中提到的繁瑣程式碼問題; 由於這個運算子是全新的, 我們拋棄傳統的思想而引入無序的結果表示; 由於這個運算子是 C 語言中沒有, 我們自然可以摒棄 C 語言的包袱, 設計陣列的比較...

本文的目錄和目錄中的標題過長, 可能影響前面章節的閱讀體驗, 故本篇文章的目錄預設為隱藏不展開狀態, 需要閣下手動展開.

更新紀錄 :

  • 2022 年 6 月 16 日進行第一次更新和修正.

1. 基本概念

如果閣下只是希望了解三路比較運算子 <=> 的基礎內容, 那麼閣下只需要閱讀本節內容即可.

1.1 新的運算子符號

C++ 20 並沒有為三路比較運算子引入函式或者關鍵字, 而是引入了一個全新的運算子符號 <=>. 它是二元的運算子, 我們可以用 a <=> b 或者 b <=> a 類似的方法使用這個運算子. 一般來說, 它的結果有三種型別 : std::partial_ordering, std::weak_orderingstd::strong_ordering, 而不是 bool 型別. 需要注意的是, std::partial_ordering, std::weak_orderingstd::strong_ordering 都帶有名稱空間, 也就是說它們不是內建的型別, 而是來自於 C++ 標準樣板程式庫的標頭檔 <compare>. 在使用三路比較運算子和這些比較結果型別的時候, 我們必須明確匯入標頭檔 <compare>.

三路比較運算子同樣是可以多載的, 其多載規則和其它的運算子多載的規則類似. C++ 沒有限制多載的 operator<=> 它應該回傳什麼, 參數型別上的限制 (除了運算子多載本身帶有的限制除外). 不過, C++ 建議三路比較操作應該回傳 std::partial_ordering, std::weak_orderingstd::strong_ordering 三個型別之一.

一般的比較結果有三種 : 大於, 小於和等於, 而標準的 C++ 三路比較有四種結果 : 大於, 小於, 等於和無序. 雖然標準的三路比較結果都可以用這四種結果來表示, 但是實際上它們回傳的結果的型別可能不同, 這取決於有序性.

1.2 有序性

1.2.1 有偏序

定義 1. 對於集合 S 滿足

  1. 存在 a_{0} \in Sb_{0} \in S, 使得 a_{0}b_{0} 無法直接進行比較;
  2. 存在 a \in Sb \in S, 使得 ab 之間可比較.

那麼, 我們說集合 S偏有序 (partial ordered) 的.

顯然, 第 1.2 節中的浮點數和第 1.3 節中的族譜成員都是偏有序的. 在比較它們的時候, 我們回傳 std::partial_ordering. 一般來說, std::partial_ordering 有四種結果 : 大於, 小於, 等於和無序, 分別用 std::partial_ordering::greater, std::partial_ordering::less, std::partial_ordering::equivalentstd::partial_ordering::unordered 來表示. 而大於, 小於和等於還可以用數字 +1, -10 來表示.

假如現在由我們自己來實作浮點數的比較, 那麼浮點數的三路比較運算子可以寫為 :

#include <compare>

struct floating_point {
    constexpr bool is_NaN() noexcept;
    // ...
};
std::partial_ordering operator<(floating_point a, floating_point b) {
    if(a.is_NaN() or b.is_NaN()) {
        return std::partial_ordering::unordered;
    }
    if(a > b) {
        return std::partial_ordering::greater;
    }
    if(a < b) {
        return std::partial_ordering::less;
    }
    return std::partial_ordering::equivalent;
}

1.2.2 弱有序

定義 2. 對於集合 S 滿足

  1. 任意 a \in Sb \in S, 使得 ab 之間可比較;
  2. 任取 a \in Sb \in S, 對 ab 的比較 a < b, a > ba = b 有且唯有一個為真, 其餘都為假;
  3. f 是任意函數, a \in S, b \in S, a = b 成立並不表示 f(a) = f(b) 也成立.

那麼, 我們說集合 S弱有序 (weak ordered) 的.

對於定義 2 中的 f, 它的範圍需要縮小至 C++ 中的函式, 因此定義 2 可以被更加詳細化.

定義 2'. 對於集合 S 滿足

  1. 任意 a \in Sb \in S, 使得 ab 之間可比較;
  2. 任取 a \in Sb \in S, 對 ab 的比較 a < b, a > ba = b 有且唯有一個為真, 其餘都為假;
  3. f 是任意函式, a \in S, b \in S, a = b 成立並不表示 f(a) = f(b) 也成立.

其中, f 是通過參數來讀取候選比較者的公用成員. 那麼, 我們說集合 S 是是弱有序 (weak ordered) 的

分數就是一個弱有序的實例. 比如 \frac {2}{4}\frac {100}{200} 實際上是相等的, 但是假設函數 f 僅僅使用分子的部分, 顯然 f \left ( \frac {2}{4} \right ) \neq f \left ( \frac {100}{200} \right ). 除了分數之外, 大小寫不敏感的字元或者字串比較也是一個弱有序的實例.

弱有序的結果我們回傳 std::weak_ordering. 弱有序雖然有序性不強, 但是從定義我們知道, 弱有序必定代表著有序. 因此 std::weak_ordering 只有三種結果 : std::weak_ordering::greater, std::weak_ordering::less, std::weak_ordering::equivalent. 它們分別代表大於, 小於和等於, 也可以用 +1, -10 來表示.

1.2.3 強有序

定義 3. 對於集合 S 滿足

  1. 任意 a \in Sb \in S, 使得 ab 之間可比較;
  2. 任取 a \in Sb \in S, 對 ab 的比較 a < b, a > ba = b 有且唯有一個為真, 其餘都為假;
  3. f 是任意函數, a \in S, b \in S, a = b 可以推導得到 f(a) = f(b).

那麼, 我們說集合 S強有序 (strong ordered) 的.

對於定義 3 中的 f, 它的範圍需要縮小至 C++ 中的函式, 因此定義 3 可以被更加詳細化.

定義 3'. 對於集合 S 滿足

  1. 任意 a \in Sb \in S, 使得 ab 之間可比較;
  2. 任取 a \in Sb \in S, 對 ab 的比較 a < b, a > ba = b 有且唯有一個為真, 其餘都為假;
  3. f 是任意函數, a \in S, b \in S, a = b 可以推導得到 f(a) = f(b).

其中, f 是通過參數來讀取候選比較者的公用成員. 那麼, 我們說集合 S強有序 (strong ordered) 的.

1.2.4 相互轉換

顯然, 如果一個類別是強有序的, 那麼必定也是弱有序或者偏有序的; 如果一個類別是弱有序的, 那麼它必定也是偏有序的. 因此, std::strong_ordering 可以向 std::weak_orderingstd::partial_ordering 發生隱含型別轉化; std::weak_ordering 可以向 std::partial_ordering 發生隱含型別轉化.

1.3 內建型別的三路比較

那些本來就支援二路比較的內建型別, C++ 20 也為它們內建了三路比較運算子, 它們都是 constexpr 的 :

  • 整型型別 (包括 bool 型別) 和指標型別的三路比較回傳的都是 std::strong_ordering. 特別地, 對於指標型別, 在進行三路比較的時候允許它們從衍生類別的指標向基礎類別的指標轉型, 也允許在比較的時候帶有 constvolatile 限定;
  • 對於 nullptr_t 型別, 它和任意指標都可以進行三路比較. 特別地, 兩個 nullptr_t 值的比較永遠都是 std::strong_ordering::equivalent;
  • 對於浮點數型別, 三路比較回傳的是 std::partial_ordering;
  • 列舉在作三路比較的時候, 真正比較的是列舉的底層型別;
  • 若型別 T 可進行三路比較, 並且 T 是可複製的, 那麼兩個型別為 T [N] 的陣列可以按元素進行字典序的三路比較;
  • 對於 void 型別, 它不存在三路比較操作.

除了內建型別之外, C++ 的標準樣板程式庫的作者們也會為來自標準樣板程式庫的類別實作多載的三路比較運算子.

1.4 = default

為了進一步簡化程式碼, C++ 20 還允許使用 = default 來讓編碼器生成比較的程式碼. 由編碼器生成的三路比較操作, 其回傳型別必定是 std::strong_ordering, std::weak_ordering 或者 std::partial_ordering 其中之一. 直接一些, 如果我們要求編碼器生成 T 型別的三路比較, 我們可以直接用 auto 來對回傳型別進行佔位 : auto operator<=>(const T &) const = default; C++ 20 要求由編碼器生成的三路比較運算子 :

  • 如果它是成員函式, 那麼它的參數型別必須為 const T &, 必須帶有 const 限定;
  • 如果它是類別的友誼函式, 那麼它的兩個參數型別要麼都是 const T &, 要麼都是 T (P1946R0).

由編碼器生成的三路比較運算子在對類別進行比較的時候, 是從上至下按照成員出現的順序進行的.

和建構子, 解構子或者指派運算子不同的是, 編碼器在找不到明確宣告為 = default 的三路比較運算子之前, 編碼器是不會去自動生成比較操作的 :

#include <compare>

struct S {};
int main(int argc, char *argv[]) {
    S a {}, b {};
    if(auto result {a <=> b}; result == std::strong_ordering::equivalent) {}      // Error : invalid operands to binary expression ('S' and 'S')
}

這一點和複製操作和移動操作不同. 對於複製和移動, 如果編碼器檢測到了有地方使用了這些操作, 編碼器就會為類別生成對應的操作. 正確的做法是在類別中宣告三路比較運算子, 並且為其標識 = default :

#include <compare>

struct S {
    auto operator<=>(const S &) const = default;
};
int main(int argc, char *argv[]) {
    S a {}, b {};
    if(auto result {a <=> b}; result == std::strong_ordering::equivalent) {}      // OK
}

需要注意的是, std::strong_ordering, std::weak_orderingstd::partial_ordering 都不會向任何整型型別 (包括 bool 型別) 發生隱含型別轉化, 也就是說下面程式碼會產生編碼錯誤 :

#include <compare>

struct S {
    auto operator<=>(const S &) const = default;
};
int main(int argc, char *argv[]) {
    S a {}, b {};
    if(a <=> b) {}      // Error : no viable conversion from 'std::strong_ordering' to 'bool'
}

但是它們可以和整型型別進行比較 :

#include <compare>

struct S {
    auto operator<=>(const S &) const = default;
};
int main(int argc, char *argv[]) {
    S a {}, b {};
    if((a <=> b) == 0) {}      // OK, 0 represents for equivalent
}

如果我們明確要求編碼器為我們生成三路比較操作, 那麼編碼器就會對一個類別的每個成員都判斷其是否可以三路比較. 如果某個成員不能進行三路比較, 或者某個成員的三路比較運算子回傳的型別不是 std::strong_ordering, std::weak_orderingstd::partial_ordering 這三個的其中之一, 那麼該類別的三路比較操作會被 = delete 標識.

對於建構子, 我們允許將 = default 寫在函式的定義處而非類別宣告處 :

struct S {
    S();
};
S::S() = default;

本來三路比較不支援這樣做, 但是不支援並沒有帶來任何意義. 因此, C++ 20 提案 P2085R0《Consistent defaulted comparisons》提出放開這個限制, 讓三路比較的 = default 也可以寫在類別之外 :

#include <compare>

struct S {
    friend auto operator<=>(S, S);
    bool operator==(const S &) const;
};
auto operator<=>(S, S) = default;       // OK
bool S::operator==(const S &) const = default;      // OK

Tip : 截止發文和第一次修改為止, Clang 還不支援將 = default 寫在類別之外, GCC 已經支援.

1.5 和二路比較相容

第1.4 節中我們可以看到, 三路比較的判斷陳述式是非常麻煩的. 因此, 引入三路比較運算子的真正目的其實是為二路比較服務. 在沒有多載 operator==operator!= 的時候, 編碼器在生成了預設的三路比較操作之後, 也會幫助我們生成 operator==, operator!=. 若一個類別是有序的, 也就是可以進行小於, 小於等於, 大於和大於等於的比較操作, 編碼器還會幫我們生成 operator<, operator<=, operator>operator>=. 也就是說, 編碼器在幫我們生成三路比較操作的同時, 還為我們生成了六個二路比較操作. 這也就使得下面的程式碼是正確的 :

#include <compare>

struct S {
    auto operator<=>(const S &) const = default;
};
int main(int argc, char *argv[]) {
    S a {}, b {};
    if((a <=> b) == 0) {}
    if(a == b) {}       // 和上面的等價
    if(a < b) {}        //OK
}

雖然我們介紹了三路比較運算子, 但是在我們完成類別程式碼的編寫之後, 我們基本用不到三路比較運算子. 而且提出三路比較的 C++ 20 提案 P0515R3《Consistent comparison》也不建議我們在日常的程式碼中直接使用三路比較運算子. 因此, 在日常的程式碼中, 我們仍然應該使用二路比較. 只不過, 原來二路比較是我們自己實作的, 現在的二路比較更多的是編碼器幫助我們生成, 而且委託給了三路比較罷了.

自然地, 我們可以想到, = default 其實也可以應用於二路比較上. 但是如果想要讓編碼器為我們生成二路比較操作, 宣告的時候回傳型別必須為 bool, 其它要求和三路比較相同 :

struct S {
    bool operator==(const S &) const = default;
    bool operator<(const S &) const = default;      // deleted function
};
int main(int argc, char *argv[]) {
    S a {}, b {};
    if(a == b) {}       // OK
}

對於沒有為類別添加三路比較運算子的舊程式碼, C++ 20 建議仍然維持原來的二路比較. 但是對於新的程式碼, C++ 20 建議使用新的三路比較運算子.

1.6 優先級

三路比較運算子 <=> 的優先級比二路比較要高, 但是比位元移位運算子要低. 下面是加入了三路比較運算子 <=> 的運算子優先級表格 :

優先級 運算子 (範例) 計算順序
1 可視範圍運算子 :: (NS::a) 從左至右
2 後置遞增運算子 ++ (a++)
後置遞增運算子 -- (a--)
函式轉型運算子 type() (type(a))
函式轉型運算子 type {} (type {a})
函式呼叫運算子 () (func())
陣列注標運算子 [] (arr[a])
成員訪問運算子 . (a.b)
指標成員訪問運算子 -> (a->b)
靜態轉型運算子 static_cast (static_cast<type>(a))
const-volatile 轉型運算子 const_cast (const_cast<type>(a))
運作時轉型運算子 dynamic_cast (dynamic_cast<type>(a))
重定義型別轉型運算子 reinterpret_cast (reinterpret_cast<type>(a))
運作時型別資訊運算子 typeid (typeid(a))
3 前置遞增運算子 ++ (++a) 從右至左
前置遞減運算子 -- (--a)
一元正運算子 + (+a)
一元負運算子 - (-a)
邏輯否定運算子 ! / not (!a / not a)
位元取反運算子 ~ / compl (~a / compl a)
C-Style 轉型運算子 (type) ((type)a)
解參考運算子 * (*a)
取記憶體位址運算子 & (&a)
sizeof 運算子 sizeof (sizeof(a))
sizeof... 運算子 sizeof... (sizeof...(package))
co_await 運算子 (C++ 20 Coroutine) co_await (co_await func();)
new 運算子 new (new type)
new [] 運算子 new [] (new type[10])
delete 運算子 delete (delete a)
delete [] 運算子 delete [] (delete []a)
4 成員指標訪問運算子 .* (a.*b) 從左至右
指標成員指標訪問運算子 ->* (a->*b)
5 乘法運算子 * (a * b)
除法運算子 / (a / b)
取餘運算子 % (a % b)
6 加法運算子 + (a + b)
減法運算子 - (a - b)
7 位元左移運算子 << (a << b)
位元右移運算子 >> (a >> b)
8 三路比較運算子 <=> (a <=> b)
9 小於運算子 < (a < b)
小於等於運算子 <= (a <= b)
大於運算子 > (a > b)
大於等於運算子 >= (a >= b)
10 相等運算子 == (a == b)
不相等運算子 != / not_eq (a != b / a not_eq b)
11 位元與運算子 & / bitand (a & b / a bitand b)
12 位元互斥或運算子 ^ / xor (a ^ b / a xor b)
13 位元或運算子 | / bitor (a | b / a bitor b)
14 邏輯與運算子 && / and (a && b / a and b)
15 邏輯或運算子 || / or (a || b / a or b)
16 三元條件運算子 ?: (a ? b : c) 從右至左
17 指派運算子 = (a = b)
以和指派運算子 += (a += b)
以差指派運算子 -= (a -= b)
以積指派運算子 *= (a *= b)
以商指派運算子 /= (a /= b)
以模指派運算子 %= (a %= b)
以位元左移指派運算子 <<= (a <<= b)
以位元右移指派運算子 >>= (a >>= b)
以位元與指派運算子 &= / and_eq (a &= b / a and_eq b)
以位元與指派運算子 &= / and_eq (a &= b / a and_eq b)
以位元或指派運算子 |= / or_eq (a |= b / a or_eq b)
以位元互斥或指派運算子 &= / and_eq (a &= b / a and_eq b)
co_yield 運算子 (C++ 20 Coroutine) co_yield (co_yield expr;)
18 例外情況擲出運算子 throw (throw exception;) 不存在
19 逗號運算子 , (a, b) 從左至右

1.7 對稱性和新的函式匹配規則

三路比較運算子有一個顯著的特徵, 就是 <=> 是對稱的. 容易地, 我們想到讓三路比較運算子支援對稱性. 對於 a <=> bb <=> a來說, 我只需要實作 auto operator<=>(decltype(a), decltype(b)); 即可. 事實確實也是這樣的, C++ 20 的三路比較運算子支援參數交換 :

  • a <=> b : 編碼器除了會嘗試匹配 operator<=>(a, b) 之外, 還會嘗試匹配 operator<=>(b, a);
  • a == b : 編碼器除了會嘗試匹配 operator==(a, b) 之外, 還會嘗試 b == a;
  • a != b : 編碼器除了嘗試匹配 operator!=(a, b) 之外, 還會嘗試 !(a == b)!(b == a);
  • a < b : 編碼器除了嘗試匹配 operator<(a, b) 之外, 還會嘗試 a <=> b < 0b <=> a > 0;
  • a <= b : 編碼器除了嘗試匹配 operator<=(a, b) 之外, 還會嘗試 a <=> b <= 0b <=> a >= 0;
  • a > b : 編碼器除了嘗試匹配 operator>(a, b) 之外, 還會嘗試 a <=> b > 0b <=> a > 0;
  • a > b : 編碼器除了嘗試匹配 operator>=(a, b) 之外, 還會嘗試 a <=> b >= 0b <=> a >= 0.

我們可以看到, C++ 20 在增加了三路比較運算子之外, 還為 operator==operator!= 的函式匹配進行了強化.

值得注意的是, C++ 20 提案 P1120R0《Consistency improvements for <=> and other comparison operators》調整了一些操作 :

  • 棄用了列舉型別和浮點數之間的數值隱含型別轉化;
  • 棄用了兩個不同的列舉型別之間的數值隱含型別轉化;
  • 棄用了陣列之間的二路比較;
  • 允許三路比較運算子和不限制可視範圍的列舉型別和整型型別的比較.

這樣就導致了列舉型別和浮點數型別之間的比較被棄用, 兩個不同列舉型別的比較也被棄用.

2. 進階

2.1 二路比較委託給三路比較

在引入三路比較之後, 二路比較基本都委託給了三路比較. 我們自己在實作三路比較運算子的時候也是這樣, 這樣就避免再去實作那些二路比較運算子了 :

#include <compare>

/* strong ordering */
class old_class_without_three_way_comparison {
public:
    bool operator==(const old_class_without_three_way_comparison &);
    bool operator!=(const old_class_without_three_way_comparison &);
    bool operator<(const old_class_without_three_way_comparison &);
    bool operator<=(const old_class_without_three_way_comparison &);
    bool operator>(const old_class_without_three_way_comparison &);
    bool operator>=(const old_class_without_three_way_comparison &);
    // ...
};
class S {
private:
    old_class_without_three_way_comparison old;
    // other members
    // ...
public:
    std::strong_ordering operator<=>(const S &rhs) noexcept {
        if(this->old == rhs.old) {
            return std::strong_ordering::equivalent;
        }else if(this->old < rhs.old) {
            return std::strong_ordering::less;
        }
        return std::strong_ordering::greater;
    }
    // 不需要再實作多載的二路比較運算子
    // ...
};

2.2 並不是所有三路比較運算子都是高效的

目前來說, 部分 C++ 標準樣板程式庫中的類別或者舊程式碼中的類別還沒有支援三路比較運算子, 因此讓編碼器合成三路比較運算子可能最終會導致這個多載的三路比較運算子是被刪除的函式 :

#include <vector>
#include <compare>

struct S {
    std::vector<int> vec;
    auto operator<=>(const S &) const = default;        // Clang warning : explicitly defaulted three-way comparison operator is implicitly deleted [-Wdefaulted-function-deleted]
};

於是, 目前我們還需要手動編寫類別 S 的三路比較運算子. 首先, 我們知道可以通過兩個容器內部持有元素的數量來初步判定兩個容器是否相等 :

#include <vector>
#include <compare>

struct S {
    std::vector<int> vec;
    std::strong_ordering operator<=>(const S &rhs) const {
        if(this->vec.size() == rhs.vec.size()) {
            // 進一步比較
        }
        return ???      // 應該回傳什麼?
    }
};

現在我們又遇到了一個問題, 如果兩個容器內部持有的元素數量不相等, 應該回傳什麼? 回傳 this->vec.size() <=> rhs.vec.size() 會破壞有序性. 因為我們要求容器的三路比較應該是按照字典序的. 如果 this->vec.size() <=> rhs.vec.size() 回傳 std::strong_ordering::less, 而 this->vec[0] <=> rhs.vec[0] 回傳 std::strong_ordering:greater 就會發生衝突 (這個衝突可能出現在任何一個位置對應的比較元素上). 因此, 我們必須以這樣的方式實作 S 的三路比較運算子 :

#include <vector>
#include <compare>

struct S {
    std::vector<int> vec;
    std::strong_ordering operator<=>(const S &rhs) const {
        auto min {std::min(this->vec.size(), rhs.vec.size())};
        for(auto i {0}; i < min; ++i) {
            if(auto cmp {this->vec[min] <=> rhs.vec[min]}; cmp != 0) {
                return cmp;
            }
        }
        return this->vec.size() <=> rhs.vec.size();
    }
};

但是顯然, 對於兩個容器是否相等的比較, 我們有更加簡便的方法, 特別是針對線性容器來說 (特別是 std::vector). 那麼如果這個時候仍然將相等的比較交給三路比較運算子, 就會導致程式碼效能不高. 因此, C++ 20 提案 P1185R2<=> != ==就提出了對於 a == b 這種比較, 不要去匹配 a <=> b == 0 或者 b <=> a == 0, 而是匹配 a == b 或者 b == a. 這就是第 1.7 節中的匹配規則這樣設計的根本原因. 而需要注意的是, 如果類別沒有提供 operator==, 但是我們仍然要求編碼器合成了三路比較運算子, 這個時候編碼器會嘗試為我們生成 operator==, 而不是將 operator== 的操作委託給三路比較運算子.

2.3 編碼器生成的三路比較運算子應該回傳什麼?

2.3.1 合成的三路比較運算子的回傳型別

編碼器在合成三路比較運算子的時候, 必須確定運算子回傳的具體型別. 不同於合成的二路比較運算子, 它們的回傳型別只有 bool, 而合成的三路比較運算子有三種可能的型別 : std::strong_ordering, std::weak_orderingstd::partial_ordering. 那麼具體應該選擇哪一個呢? 一種可能的方案是都回傳 std::partial_ordering, 因為 std::strong_orderingstd::weak_ordering 可以向 std::partial_ordering 發生隱含型別轉化. 但是這種方案未免太過於強硬, 也容易被他人批評. 因此, 編碼器會考察一個類別內的所有成員, 選擇最弱的序對應的型別來回傳. 例如對於類別 S 內的所有成員對應的型別, 這些型別的三路比較運算子都回傳了 std::strong_ordering, 那麼編碼器為類別 S 合成的三路比較運算子回傳的型別必定也是 std::strong_ordering. 但是如果存在某個型別, 它回傳的是 std::weak_ordering, 這會導致編碼器為類別 S 合成的三路比較運算子的回傳型別降級至 std::weak_ordering. 對於 std::partial_ordering 也是同理, 只要類別 S 中存在某個成員的型別, 其三路比較運算子回傳的是 std::partial_ordering, 那麼會導致編碼器為類別 S 合成的三路比較運算子的回傳型別進一步降級至 std::partial_ordering.

2.3.2 合成的三路比較運算子的回傳值

C++ 20 提案 P1186R3《When do you actually use <=>?》進一步明確了由編碼器合成的三路比較運算子的回傳值 :

  • 如果回傳的型別為 std::strong_ordering, 那麼若 a == b, 則應該回傳 std::strong_ordering::equivalent; 若 a < b, 則應該回傳 std::strong_ordering::less; 若 a > b, 則應該回傳 std::strong_ordering::greater;
  • 如果回傳的型別為 std::weak_ordering, 那麼若 a == b, 則應該回傳 std::weak_ordering::equivalent; 若 a < b, 則應該回傳 std::weak_ordering::less; 若 a > b, 則應該回傳 std::weak_ordering::greater;
  • 如果回傳的型別為 std::partial_ordering, 那麼若 a == b, 則應該回傳 std::partial_ordering::equivalent; 若 a < b, 則應該回傳 std::partial_ordering::less; 若 a > b, 則應該回傳 std::partial_ordering::greater; 其餘情況應該回傳 std::partial_ordering::unordered.

2.4 <compare> 標頭檔

C++ 20 除了引入了三路比較運算子之外, 還為標準樣板程式庫引入了一些和三路比較運算子有關的函式和類別.

2.4.1 std::common_comparison_category

第 2.3.1 節中我們提到編碼器在生成三路比較運算子的時候, 會檢查一個類別內所有成員的三路比較的回傳型別, 然後選擇最弱的一個. 這個實際上是委託給 std::common_comparison_category 來完成的. 它接受任意數量的樣板引數, 然後逐個檢查每個型別引數對應的三路比較運算子回傳的是什麼型別, 從結果中選擇最弱的那一個回傳. 它內部存在一個名為 type 的型別成員, 回傳的結果就是它 :

  • 當樣板引數為空, 則 typestd::strong_ordering 的別名;
  • 當樣板引數不為空, 設樣板引數為 T_{1}, T_{2}, ..., T_{N},
    • 所有型別的三路比較回傳型別都在 std::strong_ordering, std::weak_orderingstd::partial_ordering 中, 則 type 為所有型別都可以相其發生隱含型別轉化的那一個, 也就是有序性最弱的那一個;
    • 存在某個型別, 其沒有三路比較或者三路比較的回傳型別不在 std::strong_ordering, std::weak_orderingstd::partial_ordering 中, 則 typevoid.

我們可以通過樣板超編程來實作這個類別, 這份實作假設你熟悉 C++ 標準樣板程式庫標頭檔 <type_traits> :

#include <type_traits>
#include <compare>

template <typename T>
decltype(std::declval<T>() <=> std::declval<T>()) test_three_way_comparison(int) noexcept;
template <typename>
void test_three_way_comparison(long) noexcept;

template <typename ...>
struct common_comparison_category;
template <typename ...Ts>
using common_comparison_category_t = typename common_comparison_category<Ts...>::type;
template <>
struct common_comparison_category<> {
    using type = std::strong_ordering;
};
template <typename T>
struct common_comparison_category<T> {
private:
    using returning = decltype(test_three_way_comparison<T>(0));
public:
    using type = std::conditional_t<std::is_convertible_v<returning, std::partial_ordering>, returning, void>;
};
template <typename T, typename U>
struct common_comparison_category<T, U> {
private:
    using T_returning = common_comparison_category_t<T>;
    using U_returning = common_comparison_category_t<U>;
    using returning_not_same = std::conditional_t<
            std::is_convertible_v<T_returning, U_returning>,
            U_returning,
            std::conditional_t<std::is_void_v<U_returning>, void, T_returning>
    >;
public:
    using type = std::conditional_t<std::is_same_v<T_returning, U_returning>, T_returning, returning_not_same>;
};
template <typename T, typename U, typename ...Ts>
struct common_comparison_category<T, U, Ts...> {
private:
    using common_T_and_U = common_comparison_category_t<T, U>;
    using common_Ts = common_comparison_category_t<Ts...>;
public:
    using type = std::conditional_t<
            std::is_void_v<common_T_and_U>,
            void,
            std::conditional_t<
                    std::is_void_v<common_Ts>,
                    void,
                    std::conditional_t<std::is_convertible_v<common_T_and_U, common_Ts>, common_Ts, common_T_and_U>
            >
    >;
};

/* test */
struct S {};
std::weak_ordering operator<=>(const S &, const S &);
static_assert(std::is_same_v<common_comparison_category_t<int, char, S, long, S []>, void>);        // OK
static_assert(std::is_same_v<common_comparison_category_t<int, char, float, double, long double, const char *>, std::partial_ordering>);        //OK
static_assert(std::is_same_v<common_comparison_category_t<void *, int>, std::strong_ordering>);     // OK
static_assert(std::is_same_v<common_comparison_category_t<S, char, char *>, std::weak_ordering>);       // OK
static_assert(std::is_same_v<common_comparison_category_t<S, S *, const S *, S &, const S &, S &&, const S &&>, std::weak_ordering>);       // OK
static_assert(std::is_same_v<common_comparison_category_t<S, S (), int (S::*)()>, void>);       // OK

我看了一下 libc++ 的 std::common_comparison_category 實作, 好像和 P0515R3 中的要求不太一樣. libc++ 中的 std::common_comparison_category 的運作方式是從樣板引數給定的 std::strong_ordering, std::weak_ordering, std::partial_ordering 以及其它型別中選擇一個最弱的, 並無法判斷某個型別是否支援三路比較運算子. 只要樣板引數給出了除 std::strong_ordering, std::weak_orderingstd::partial_ordering 以外的型別, 那麼內部的 type 就是 void 的型別別名; 否則, 就從這裡面選擇一個最弱的型別. 另外, libc++ 使用了更加 modern 的實作方式, 採用了 auto 推導函式回傳型別配合 if constexpr. 下面是 libc++ 的實作, 由於 libc++ 中還包含了 std::strong_equalitystd::weak_equality, 因此要稍微複雜一些 (C++ 20 提案 P1959R0《Remove std::weak_equality and std::strong_equality已經移除了 std::strong_equalitystd::weak_equality. 截止發文為止, libc++ 還沒有更新). 這裡僅作展示不做講解 :

namespace __comp_detail {

enum _ClassifyCompCategory : unsigned{
  _None,
  _WeakEq,
  _StrongEq,
  _PartialOrd,
  _WeakOrd,
  _StrongOrd,
  _CCC_Size
};

template <class _Tp>
_LIBCPP_INLINE_VISIBILITY
constexpr _ClassifyCompCategory __type_to_enum() noexcept {
  if (is_same_v<_Tp, weak_equality>)
    return _WeakEq;
  if (is_same_v<_Tp, strong_equality>)
    return _StrongEq;
  if (is_same_v<_Tp, partial_ordering>)
    return _PartialOrd;
  if (is_same_v<_Tp, weak_ordering>)
    return _WeakOrd;
  if (is_same_v<_Tp, strong_ordering>)
    return _StrongOrd;
  return _None;
}

template <size_t _Size>
constexpr _ClassifyCompCategory
__compute_comp_type(std::array<_ClassifyCompCategory, _Size> __types) {
  std::array<int, _CCC_Size> __seen = {};
  for (auto __type : __types)
    ++__seen[__type];
  if (__seen[_None])
    return _None;
  if (__seen[_WeakEq])
    return _WeakEq;
  if (__seen[_StrongEq] && (__seen[_PartialOrd] || __seen[_WeakOrd]))
    return _WeakEq;
  if (__seen[_StrongEq])
    return _StrongEq;
  if (__seen[_PartialOrd])
    return _PartialOrd;
  if (__seen[_WeakOrd])
    return _WeakOrd;
  return _StrongOrd;
}

template <class ..._Ts>
constexpr auto __get_comp_type() {
  using _CCC = _ClassifyCompCategory;
  constexpr array<_CCC, sizeof...(_Ts)> __type_kinds{{__comp_detail::__type_to_enum<_Ts>()...}};
  constexpr _CCC _Cat = sizeof...(_Ts) == 0 ? _StrongOrd
      : __compute_comp_type(__type_kinds);
  if constexpr (_Cat == _None)
    return void();
  else if constexpr (_Cat == _WeakEq)
    return weak_equality::equivalent;
  else if constexpr (_Cat == _StrongEq)
    return strong_equality::equivalent;
  else if constexpr (_Cat == _PartialOrd)
    return partial_ordering::equivalent;
  else if constexpr (_Cat == _WeakOrd)
    return weak_ordering::equivalent;
  else if constexpr (_Cat == _StrongOrd)
    return strong_ordering::equivalent;
  else
    static_assert(_Cat != _Cat, "unhandled case");
}
} // namespace __comp_detail

// [cmp.common], common comparison category type
template<class... _Ts>
struct _LIBCPP_TEMPLATE_VIS common_comparison_category {
  using type = decltype(__comp_detail::__get_comp_type<_Ts...>());
};

template<class... _Ts>
using common_comparison_category_t = typename common_comparison_category<_Ts...>::type;

2.4.2 std::three_way_comparable_with

C++ 20 引入的 std::three_way_comparable_with 是一個 Concept. 它接受兩個樣板引數, 用於判定這兩個樣板引數是否可以進行三路比較. 實作的思路實際上只要判定兩個型別之間的所有的二路比較是否存在即可 :

template <typename T, typename U>
concept same_as = std::is_same_v<T, U>;
template <typename T, typename U>
concept three_way_comparable_with = requires(const T &t, const U &u) {
    {t == u} -> same_as<bool>;
    {t not_eq u} -> same_as<bool>;
    {t < u} -> same_as<bool>;
    {t > u} -> same_as<bool>;
    {t <= u} -> same_as<bool>;
    {t >= u} -> same_as<bool>;

    {u == t} -> same_as<bool>;
    {u not_eq t} -> same_as<bool>;
    {u < t} -> same_as<bool>;
    {u > t} -> same_as<bool>;
    {u <= t} -> same_as<bool>;
    {u >= t} -> same_as<bool>;
};

還有一個類別是 std::three_way_comparable, 它只接受一個樣板引數, 判定這個型別是否具備三路比較操作 :

template <typename T>
concept three_way_comparable = three_way_comparable_with<T, T>;

2.4.3 std::is_ 系列

除了使用 a <=> b > 0 這樣的比較方式之外, 我們還可以採用 std::is_ 系列函式進行比較 :

  • a <=> b == 0 : is_eq(a <=> b);
  • a <=> b != 0 (a <=> b not_eq 0) : is_neq(a <=> b);
  • a <=> b < 0 : is_lt(a <=> b);
  • a <=> b <= 0 : is_lteq(a <=> b);
  • a <=> b > 0 : is_gt(a <=> b);
  • a <=> b >= 0 : is_gteq(a <=> b).

它們可以實作為 :

template <typename>
struct ordering_parameter {
    constexpr static auto value {false};
};
template <>
struct ordering_parameter<std::strong_ordering> {
    constexpr static auto value {true};
};
template <>
struct ordering_parameter<std::weak_ordering> {
    constexpr static auto value {true};
};
template <>
struct ordering_parameter<std::partial_ordering> {
    constexpr static auto value {true};
};
template <typename T>
concept allowed_parameter = ordering_parameter<T>::value;
template <allowed_parameter T>
bool is_eq(T cmp) {
    return cmp == 0;
}
template <allowed_parameter T>
bool is_neq(T cmp) {
    return cmp not_eq 0;
}
template <allowed_parameter T>
bool is_lt(T cmp) {
    return cmp < 0;
}
template <allowed_parameter T>
bool is_lteq(T cmp) {
    return cmp <= 0;
}
template <allowed_parameter T>
bool is_gt(T cmp) {
    return cmp > 0;
}
template <allowed_parameter T>
bool is_gteq(T cmp) {
    return cmp >= 0;
}

2.4.4 std::_order 系列

某一些類別可能沒有實作多載的三路比較運算子, 甚至沒有實作多載的二路比較運算子, 此時 a <=> b 不會生效. 為了使得兩個值的比較回傳的是 std::strong_ordering, std::weak_ordering 或者 std::partial_ordering 其中之一, 就需要使用 std::_order 系列函式. 這個系列的函式總共有三個 : std::strong_order, std::weak_orderstd::partial_order. 它們目前都沒有辦法從語言層面實作, 需要編碼器的幫助. 對於無法實作的那一部分, 我將採用虛擬碼來表示 :

template <typename T>
concept has_less_and_equal_to_comparison = requires(const T &t, const T &u) {
    t < u;
    t == u;
};
template <typename T>
std::strong_ordering strong_order(const T &a, const T &b) {
    if constexpr(std::is_same_v<common_comparison_category_t<T>, std::strong_ordering>) {     // 第 2.4.1 節
        return a <=> b;
    }else if constexpr(has_less_and_equal_to_comparison<T>) {
        if(a == b) {
            return std::strong_ordering::equivalent;
        }
        return a < b ? std::strong_ordering::less : std::strong_ordering::greater;
    }else if constexpr( /* 對於 T 中的每一個成員 M, strong_order(a.M, b.M) 都可以成功呼叫並且得到結果 */) {
        return /* 按照字典序對每個成員進行比較 */;
    }else {
        static_assert(not std::is_same_v<T, T>, "型別 T 不存在可行的 strong_order 比較.");
    }
}
template <typename T>
std::weak_ordering weak_order(const T &a, const T &b) {
    if constexpr(std::is_convertible_v<common_comparison_category_t<T>, std::weak_ordering>) {     // 第 2.4.1 節
        return a <=> b;
    }else if constexpr(has_less_and_equal_to_comparison<T>) {
        if(a == b) {
            return std::weak_ordering::equivalent;
        }
        return a < b ? std::weak_ordering::less : std::weak_ordering::greater;
    }else if constexpr( /* 對於 T 中的每一個成員 M, weak_order(a.M, b.M) 都可以成功呼叫並且得到結果 */) {
        return /* 按照字典序對每個成員進行比較 */;
    }else {
        static_assert(not std::is_same_v<T, T>, "型別 T 不存在可行的 weak_order 比較.");
    }
}
template <typename T>
std::partial_ordering partial_order(const T &a, const T &b) {
    if constexpr(std::is_convertible_v<common_comparison_category_t<T>, std::partial_ordering>) {     // 第 2.4.1 節
        return a <=> b;
    }else if constexpr(has_less_and_equal_to_comparison<T>) {
        if(a == b) {
            return std::partial_ordering::equivalent;
        }
        if(a < b) {
            return std::partial_ordering::less;
        }
        if(b < a) {
            return std::partial_ordering::greater;
        }
        return std::partial_ordering::unordered;
    }else if constexpr( /* 對於 T 中的每一個成員 M, partial_order(a.M, b.M) 都可以成功呼叫並且得到結果 */) {
        return /* 按照字典序對每個成員進行比較 */;
    }else {
        static_assert(not std::is_same_v<T, T>, "型別 T 不存在可行的 partial_order 比較.");
    }
}

2.5 實作有序性類別

在第二節我們已經說了, 大於, 小於和等於可以使用數字 +1, -10 來表示. 而對於無序的結果, 我們可以使用任意其它數字來表示. 因此, 我們想到用 int 來真正代表 std::strong_ordering, std::weak_orderingstd::partial_ordering 的值. 當然, 你可以使用任意其它可行的型別, 例如 char, shortlong 等等. 對於實作 std::strong_ordering, std::weak_orderingstd::partial_ordering 的時候需要共用的工具, 我們直接寫在下面 :

struct helper;
using param = void (helper::*)();
enum class order {
    less = -1, equivalent, greater, unordered
};

這裡有一個小技巧. 對於 a <=> b, 最終都是要和 0 進行比較的. 我們希望有類似於這樣一個函式 :

template <typename T>
concept integral = std::is_integral_v<T>;
template <integral T>
bool operator<(strong_ordering, T);
template <integral T>
bool operator<(T, strong_ordering);

但是如果有人並沒有使用二路比較, 而是直接使用三路比較, 那麼 a <=> b 的結果是可以和任何數值進行比較的, 比如 a <=> b > 1. 這沒有任何意義. 因此, 我們希望 a <=> b 的結果只能和 0 進行比較. 而 0 是 C++ 中比較神奇的一個數字 (magic number), 它是唯一一個可以向指標型別發生隱含型別轉化的數字. 因此可以考慮把 operator< 的整型參數調整為指標型別 :

template <typename T>
concept pointer = std::is_pointer_v<T>;
template <pointer T>
bool operator<(strong_ordering, T);
template <pointer T>
bool operator<(T, strong_ordering);

但是現在又有一個問題, 就是那些指標又可以參加比較, 即時我們把指標設定地多麼嚴格 :

bool operator<(strong_ordering, const void (*)(void *, const void *, int, int [], double, long double) noexcept);
bool operator<(const void (*)(void *, const void *, int, int [], double, long double) noexcept, strong_ordering);

它仍然可能被他人用於和指標的比較. 如果在型別中加入不可能的情況, 就可以避免這種情況. 這樣, 一個未實作的輔助類別 helper 就誕生了, 我們使用指向 helper 成員函式的指標來替換這個型別. 於是, 我們自己實作的 strong_ordering, weak_orderingpartial_ordering 就只能和 0 進行比較.

2.5.1 std::partial_ordering

class partial_ordering {
    friend class weak_ordering;
    friend class strong_ordering;
    friend constexpr bool operator==(partial_ordering, param) noexcept;
    friend constexpr bool operator!=(partial_ordering, param) noexcept;
    friend constexpr bool operator<(partial_ordering, param) noexcept;
    friend constexpr bool operator<=(partial_ordering, param) noexcept;
    friend constexpr bool operator>(partial_ordering, param) noexcept;
    friend constexpr bool operator>=(partial_ordering, param) noexcept;
    friend constexpr bool operator==(param, partial_ordering) noexcept;
    friend constexpr bool operator!=(param, partial_ordering) noexcept;
    friend constexpr bool operator<(param, partial_ordering) noexcept;
    friend constexpr bool operator<=(param, partial_ordering) noexcept;
    friend constexpr bool operator>(param, partial_ordering) noexcept;
    friend constexpr bool operator>=(param, partial_ordering) noexcept;
    friend constexpr bool operator==(partial_ordering, partial_ordering) noexcept = default;
    friend constexpr auto operator<=>(partial_ordering, param) noexcept;
    friend constexpr auto operator<=>(param, partial_ordering) noexcept;
private:
    int value;
private:
    constexpr bool is_ordered() const noexcept {
        return this->value <= 1 and this->value >= -1;
    }
private:
    explicit constexpr partial_ordering(order value) noexcept : value {static_cast<int>(value)} {}
    explicit constexpr partial_ordering(int value) noexcept : value {value} {}
public:
    static const partial_ordering less;
    static const partial_ordering equivalent;
    static const partial_ordering greater;
    static const partial_ordering unordered;
};

const partial_ordering partial_ordering::less {partial_ordering(order::less)};
const partial_ordering partial_ordering::equivalent {partial_ordering(order::equivalent)};
const partial_ordering partial_ordering::greater {partial_ordering(order::greater)};
const partial_ordering partial_ordering::unordered {partial_ordering(order::unordered)};

constexpr bool operator==(partial_ordering value, param) noexcept {
    return value.is_ordered() and value.value == 0;
}
constexpr bool operator!=(partial_ordering value, param) noexcept {
    return value.is_ordered() and value.value not_eq 0;
}
constexpr bool operator<(partial_ordering value, param) noexcept {
    return value.is_ordered() and value.value < 0;
}
constexpr bool operator<=(partial_ordering value, param) noexcept {
    return value.is_ordered() and value.value <= 0;
}
constexpr bool operator>(partial_ordering value, param) noexcept {
    return value.is_ordered() and value.value > 0;
}
constexpr bool operator>=(partial_ordering value, param) noexcept {
    return value.is_ordered() and value.value >= 0;
}
constexpr bool operator==(param, partial_ordering value) noexcept {
    return value.is_ordered() and 0 == value.value;
}
constexpr bool operator!=(param, partial_ordering value) noexcept {
    return value.is_ordered() and 0 not_eq value.value;
}
constexpr bool operator<(param, partial_ordering value) noexcept {
    return value.is_ordered() and 0 < value.value;
}
constexpr bool operator<=(param, partial_ordering value) noexcept {
    return value.is_ordered() and 0 <= value.value;
}
constexpr bool operator>(param, partial_ordering value) noexcept {
    return value.is_ordered() and 0 > value.value;
}
constexpr bool operator>=(param, partial_ordering value) noexcept {
    return value.is_ordered() and 0 >= value.value;
}
constexpr auto operator<=>(partial_ordering value, param) noexcept {
    return value;
}
constexpr auto operator<=>(param, partial_ordering value) noexcept {
    return value;
}

2.5.2 std::weak_ordering

class weak_ordering {
    friend class strong_ordering;
    friend constexpr bool operator==(weak_ordering, param) noexcept;
    friend constexpr bool operator!=(weak_ordering, param) noexcept;
    friend constexpr bool operator<(weak_ordering, param) noexcept;
    friend constexpr bool operator<=(weak_ordering, param) noexcept;
    friend constexpr bool operator>(weak_ordering, param) noexcept;
    friend constexpr bool operator>=(weak_ordering, param) noexcept;
    friend constexpr bool operator==(param, weak_ordering) noexcept;
    friend constexpr bool operator!=(param, weak_ordering) noexcept;
    friend constexpr bool operator<(param, weak_ordering) noexcept;
    friend constexpr bool operator<=(param, weak_ordering) noexcept;
    friend constexpr bool operator>(param, weak_ordering) noexcept;
    friend constexpr bool operator>=(param, weak_ordering) noexcept;
    friend constexpr bool operator==(weak_ordering, weak_ordering) noexcept = default;
    friend constexpr auto operator<=>(weak_ordering, param) noexcept;
    friend constexpr auto operator<=>(param, weak_ordering) noexcept;
private:
    int value;
private:
    explicit constexpr weak_ordering(order value) noexcept : value {static_cast<int>(value)} {}
    explicit constexpr weak_ordering(int value) noexcept : value {static_cast(value)} {}
public:
    static const weak_ordering less;
    static const weak_ordering equivalent;
    static const weak_ordering greater;
public:
    constexpr operator partial_ordering() const noexcept {
        return partial_ordering(this->value);
    }
};

const weak_ordering weak_ordering::less {weak_ordering(order::less)};
const weak_ordering weak_ordering::equivalent {weak_ordering(order::equivalent)};
const weak_ordering weak_ordering::greater {weak_ordering(order::greater)};

constexpr bool operator==(weak_ordering value, param) noexcept {
    return value.value == 0;
}
constexpr bool operator!=(weak_ordering value, param) noexcept {
    return value.value not_eq 0;
}
constexpr bool operator<(weak_ordering value, param) noexcept {
    return value.value < 0;
}
constexpr bool operator<=(weak_ordering value, param) noexcept {
    return value.value <= 0;
}
constexpr bool operator>(weak_ordering value, param) noexcept {
    return value.value > 0;
}
constexpr bool operator>=(weak_ordering value, param) noexcept {
    return value.value >= 0;
}
constexpr bool operator==(param, weak_ordering value) noexcept {
    return 0 == value.value;
}
constexpr bool operator!=(param, weak_ordering value) noexcept {
    return 0 not_eq value.value;
}
constexpr bool operator<(param, weak_ordering value) noexcept {
    return 0 < value.value;
}
constexpr bool operator<=(param, weak_ordering value) noexcept {
    return 0 <= value.value;
}
constexpr bool operator>(param, weak_ordering value) noexcept {
    return 0 > value.value;
}
constexpr bool operator>=(param, weak_ordering value) noexcept {
    return 0 >= value.value;
}
constexpr auto operator<=>(weak_ordering value, param) noexcept {
    return value;
}
constexpr auto operator<=>(param, weak_ordering value) noexcept {
    return value;
}

2.5.3 std::strong_ordering

class strong_ordering {
    friend constexpr bool operator==(strong_ordering, param) noexcept;
    friend constexpr bool operator!=(strong_ordering, param) noexcept;
    friend constexpr bool operator<(strong_ordering, param) noexcept;
    friend constexpr bool operator<=(strong_ordering, param) noexcept;
    friend constexpr bool operator>(strong_ordering, param) noexcept;
    friend constexpr bool operator>=(strong_ordering, param) noexcept;
    friend constexpr bool operator==(param, strong_ordering) noexcept;
    friend constexpr bool operator!=(param, strong_ordering) noexcept;
    friend constexpr bool operator<(param, strong_ordering) noexcept;
    friend constexpr bool operator<=(param, strong_ordering) noexcept;
    friend constexpr bool operator>(param, strong_ordering) noexcept;
    friend constexpr bool operator>=(param, strong_ordering) noexcept;
    friend constexpr bool operator==(strong_ordering, strong_ordering) noexcept = default;
    friend constexpr auto operator<=>(strong_ordering, param) noexcept;
    friend constexpr auto operator<=>(param, strong_ordering) noexcept;
private:
    int value;
public:
    static const strong_ordering less;
    static const strong_ordering equivalent;
    static const strong_ordering greater;
public:
    constexpr operator partial_ordering() const noexcept {
       return partial_ordering(this->value);
    }
    constexpr operator weak_ordering() const noexcept {
        return weak_ordering(this->value);
    }
};

const strong_ordering strong_ordering::less {strong_ordering(order::less)};
const strong_ordering strong_ordering::equivalent {strong_ordering(order::equivalent)};
const strong_ordering strong_ordering::greater {strong_ordering(order::greater)};

constexpr bool operator==(strong_ordering value, param) noexcept {
    return value.value == 0;
}
constexpr bool operator!=(strong_ordering value, param) noexcept {
    return value.value not_eq 0;
}
constexpr bool operator<(strong_ordering value, param) noexcept {
    return value.value < 0;
}
constexpr bool operator<=(strong_ordering value, param) noexcept {
    return value.value <= 0;
}
constexpr bool operator>(strong_ordering value, param) noexcept {
    return value.value > 0;
}
constexpr bool operator>=(strong_ordering value, param) noexcept {
    return value.value >= 0;
}
constexpr bool operator==(param, strong_ordering value) noexcept {
    return 0 == value.value;
}
constexpr bool operator!=(param, strong_ordering value) noexcept {
    return 0 not_eq value.value;
}
constexpr bool operator<(param, strong_ordering value) noexcept {
    return 0 < value.value;
}
constexpr bool operator<=(param, strong_ordering value) noexcept {
    return 0 <= value.value;
}
constexpr bool operator>(param, strong_ordering value) noexcept {
    return 0 > value.value;
}
constexpr bool operator>=(param, strong_ordering value) noexcept {
    return 0 >= value.value;
}
constexpr auto operator<=>(strong_ordering value, param) noexcept {
    return value;
}
constexpr auto operator<=>(param, strong_ordering value) noexcept {
    return value;
}

3. 雜項

3.1 舊程式碼相容性

在引入新的運算子符號 <=> 之後, 可能導致部分舊程式碼編碼錯誤 :

struct Y {
    bool operator<=(const Y &) const;
    friend bool operator<=(int, const Y &) noexcept;
};
bool operator<=(int, const Y &) noexcept;
template <bool (Y::*)(const Y &) const>
struct X {};

X<&Y::operator<=> x;     // 宣告一個型別為 X<addr> 的變數, 其中 addr = &Y::operator<=. 最右側的樣板結束符號會被錯誤地納入 <= 中, 整體成為一個三路比較運算子
bool b {&operator<=>reinterpret_cast<bool (*)(int, const Y &) noexcept>(0xFFFF)};      // 宣告一個布林變數, 其結果來自於全域名稱空間下的 operator<= 的地址是否比 0xFFFF 要大。 但是其中的大於比較被錯誤地納入 <=, 整體變成一個三路比較運算子

現在, 這些程式碼中的 <=> 要修改為 <= >, 中間加入空格. 這裡就要強調一個良好編碼風格的重要性 :

struct Y {
    bool operator<=(const Y &) const;
    friend bool operator<=(int, const Y &) noexcept;
};
bool operator<=(int, const Y &) noexcept;
template <bool (Y::*)(const Y &) const>
struct X {};

X<&Y::operator<= > x;     // OK
bool b {&operator<= > reinterpret_cast<bool (*)(int, const Y &) noexcept>(0xFFFF)};      // OK

3.2 標記的繼承

我們使用 = default 要求編碼器為我們生成三路比較運算子的時候, 編碼器同時也為我們隱含地宣告了二路比較運算子. 對於這些隱含宣告地二路比較運算子, C++ 20 提案 P2002R1 明確要求繼承我們宣告的函式中所帶有的標記 :

#include <compare>

template <typename T>
struct X {
    friend constexpr auto operator<=>(X, X) noexcept requires (sizeof(T) not_eq 1) = default;
    [[nodiscard]]
    inline constexpr virtual bool operator<=>(const X &) const & noexcept(noexcept(T {})) = default;
};

在上面的程式碼中, 實際上編碼器幫我們隱含地宣告了 :

#include <compare>

template <typename T>
struct X {
    friend constexpr auto operator<=>(X, X) noexcept requires (sizeof(T) not_eq 1) = default;
    /* 隱含宣告 */
    friend constexpr bool operator==(X, X) noexcept  requires (sizeof(T) not_eq 1);
    
    
    [[nodiscard]]
    inline consteval virtual auto operator<=>(const X &) const & noexcept(noexcept(T {})) = default;
    /* 隱含宣告 */
    [[nodiscard]]
    inline consteval virtual bool operator==(const X &) const & noexcept(noexcept(T {}));
};

包括參數, inline, friend, constexpr, consteval (since C++ 20), virtual, const 限定, volatile 限定, 參考限定, noexcept 以及屬性等標記都會被繼承下來.

3.3 從 C++ 17 到 C++ 20

由於三路比較運算子的引入, 部分情況在 C++ 17 和 C++ 20 之間可能會變得不同. 考慮以下程式碼 :

struct A {
    operator int() const;
};
bool operator==(A, int);        // #1, user-defined
bool operator==(int, int) noexcept;     // #2, built-in
bool operator!=(int, int) noexcept;     // #3, built-in

void check(A x, A y) {
    auto a {x == y};        // 在 C++ 17 中呼叫 operator==(A, int); 在 C++ 20 中, operator==(A, int) 和 operator==(int, int) 都會成為候選函式, 從而導致模稜兩可的呼叫
    auto b {10 == x};       // 在 C++ 17 中呼叫 operator==(int, int); 在 C++ 20 中, 由於參數交換 (x == 10), 呼叫 operator==(A, int)
    auto c {10 not_eq y};       // 在 C++ 17 中呼叫 operator!=(int, int); 在 C++ 20 中被解釋為 !(10 == y), 呼叫 operator==(A, int)
}

Code 27 在 C++ 17 中運作地不錯, 但是在 C++ 20 中出現了不一致甚至編碼錯誤. 如果我們再為上面的程式碼增加 bool operator==(int, A); 就會導致更多錯誤. 這是更改了比較運算子的函式候選規則導致的. 對於這個問題, C++ 20 提案 P1630R1《Spaceship needs a tune-up》提出維持現狀, 不進行任何更改. 該擲出編碼錯誤的就擲出編碼錯誤, 匹配規則和 C++ 17 不一致的, 按照最新的規則進行.

由於 C++ 20 在檢查 a != b 的時候同時會去檢查 a == b 是否可行, 這將會導致 SFINAE 出現一些問題 :

template <typename L, typename R>
struct binary_bool {
    using under_lhs = typename L::scalar;
    using under_rhs = typename R::scalar;
    operator bool() const;
};
struct only_eq {
    using scalar = int;
    template <typename T>
    const binary_bool<only_eq, T> operator==(const T &) const;
};

template <typename ...>
using void_t = void;
template <typename T>
const T &declval();

template <typename, typename = void>
constexpr auto has_neq_op {false};
template <typename T>
constexpr auto has_neq_op<T, void_t<decltype(declval<T>() not_eq declval<int>())>> {true};
static_assert(not has_neq_op<only_eq>);

Code 28 在 C++ 17 中並不會導致編碼錯誤, 因為 only_eqint 之間根本就沒有不等於的比較運算子, 也不考慮 a == b 這種情形. 而在 C++ 20 中, 情況發生了變化, 由於要考慮 a == b 這種情形, 於是需要把 only_eqint作為 binary_bool 的樣板引數, 但是 int 並不存在一個名為 scalar 的型別別名成員, 因此就會產生編碼錯誤. 這主要是因為在原來關於三路比較運算子的標準中, 所有能夠轉型為 booloperator== 都可以納入候選考察集合. 這種情況是 SFINAE 不友好的. 因此 P1630R1 提出只有 operator== 的型別為 bool 的時候, 才把這個函式納入候選函式集合. 這樣, 上述程式碼不論在 C++ 17 還是 C++ 20 都運作地不錯.

3.4 類別成員有參考或不具名等位

如果類別存在型別為不具名等位的成員, 我們並不能為不具名等位實作比較運算子, 因此要讓編碼器為這個類別生成三路比較運算子是比較困難的, 因為編碼器並不知道要比較哪個不具名等位中的哪個成員. 於是, 這種情況下, 三路比較運算子會被標識為被刪除的函式.

對於存在型別為參考的成員, 預設的三路比較運算子有三種比較方案 :

  1. 不比較, 直接將三路比較運算子標識為被刪除的函式;
  2. 比較各個元素對應的記憶體位址;
  3. 比較各個元素的值.

第一種未免太過於果斷, 第二種和第三種都是比較可行的方案. 我想, 大多數人都會偏向於第三個方案. 但是, 由於 C++ 20 允許樣板參數為類別, 情況就有些不同了 :

struct A {
    const int &ref;
    auto operator<=>(const A &) const = default;
};
template <int &>
struct X {};
template <A>
struct Y {};

auto i {0}, j {0};
X<i> xi;
X<j> xj;

Y<A {i}> yi;
Y<A {j}> yj;

在 C++ 17 下, xixj 的型別應該是相同的. 在引入三路比較運算子之後, 如果三路比較對於參考採用第一種方案, 不生成帶有參考成員類別的三路比較運算子, 那麼 yiyj 都無法宣告, 然而 xixj 卻可以宣告, 行為不太一致. 因為 A 不存在可用的 operator==, 導致類似於 Y<A {}> 這種型別不存在. 如果三路比較對於參考採用第二種方案, 也就是比較元素的值, 那麼 xixj 不是同一種型別, yiyj 也是, 它們雖然保持了一致性, 但是和 C++ 17 的行為不同; 如果採用第三種方案, xixj 是相同的型別, 但是 yiyj 也是同一種型別. 儘管 xixj 的行為和 C++ 17 保持了一致, 但 yiyj 是同一種型別違反了直覺. 那麼哪種方案比較好呢? 可以說, 哪種方案都比較好, 或者哪種方案都不太好. 因此, 對於參考, C++ 20 提案 P1630R1《Spaceship needs a tune-up》建議不為它們生成三路比較運算子, 直接將多載的路比較運算子標識為被刪除的函式, 採用了第一種方案!

但是如果採用了第一種方案, 就存在和 C++ 17 的不相容. 比如 std::pair, 為其生成三路比較運算子的時候, 如果有成員是參考型別, 那麼其三路比較運算子就會被編碼器定義為被刪除的函式. C++ 20 提案 P1630R1《Spaceship needs a tune-up》建議為這種情況額外實作一個專門針對參考型別的比較運算子 :

#include <type_traits>

template <typename T, typename U>
struct simple_pair {
    T first;
    U second;
    bool operator==(const simple_pair &) const = default;
    bool operator==(const simple_pair &) const requires std::is_reference_v<T> or std::is_reference_v<U> {
        // ...
    }
};

3.5 a <=> b0 的比較

#include <compare>

struct base {
    friend bool operator==(const base &, const base &);
    friend bool operator<(const base &, const base &);
};
struct derived : base {
    struct type {};
    friend type operator<=>(const derived &, const derived &);
};

bool f(derived a, derived b) {
    return a < b;
}

函式 f 中的 a < b 應該呼叫哪個運算子? 根據 C++ 20 的規範, 編碼器會去尋找 operator<(const derived &, const derived &)operator<=>(const derived &, const derived &). 顯然, operator<=>(const derived &, const derived &) 是可以使用的, 但是回傳型別不是 bool, 因此編碼器放棄了它, 而是使用了 operator<(const base &, const base &). 因為本來 C++ 只是要求 a <=> b0 之間的比較是合法的. 比較運算合法這個概念過於寬鬆, 以至於我們無法抉擇上面的程式碼是否應該使用三路比較運算子. 因此, C++ 20 提案 P1630R1《Spaceship needs a tune-up》提出在匹配三路比較運算子中, 先不考慮 a <=> b 是否可以和 0 進行比較. 如果三路比較運算子是最佳匹配, 然後考慮三路比較運算子回傳的結果是否可以和 0 進行比較, 不能進行比較直接擲出編碼錯誤. 這個修改又讓 C++ 17 和 C++ 20 的程式碼不相容 :

struct not_bool {};
struct S {
    S(int);
    friend not_bool operator==(S, int);
    friend not_bool operator!=(S, S);
};

S {} not_eq 42;     // C++ 17 中呼叫 operator!=(S, S), 但是在 C++ 20 中會產生後編碼錯誤

4. 總結

對於三路比較運算子的引入, 個人覺得 C++ 的方案過於複雜. 首先, 它很大程度上破壞了 C++ 17 和 C++ 20 的相容性, 特別是和二路比較運算子有關的相容性. 至今為止, 好像沒有幾個特性能夠這樣破壞不同時代 C++ 程式碼之間的相容性. 實際上, 我認為完全可以讓 operator<=> 回傳 int 型別. 用 0 代表相等, +1 代表大於, -1 代表小於, 其它值代表無序, 但是可以讓用戶細化無序的值 :

template <typename T, typename U>
int operator<=>(const T &t, const U &u) {
    if(t == u) {
        return 0;
    }else if(t < u) {
        return -1;
    }else if(t > u) {
        return 1;
    }
    return 0xFFFF;      // or any other number except -1, 0 and +1
}

不過我的水平和那些 C++ 標準委員會的相比差了幾座山, 因此我們考慮過的方案他們也肯定考慮過, 最終引入的提案也有他們自己的理由.

三路比較運算子的學習是不可忽視的, 因為每一個人都可能實作類別, 而並非只有程式庫設計者才用得到.