默认比较 (C++20 起)
可以通過將比較運算符函數顯式預置的方式要求編譯器為某個類生成對應的默認比較。
定義
預置比較運算符函數 是滿足以下所有條件的非模板比較運算符函數(即 <=>, ==, !=, <, >, <=, or >=):
- 它是某個類
C的非靜態成員或友元。 - 它是在
C中或在C是完整類型的語境中定義為預置的。 - 它有兩個類型是
const C&的形參或兩個類型是C的形參,其中隱式對象形參(如果存在)會被視為第一個形參。
這樣的比較運算符函數也被稱為關於類 C 的預置比較運算符函數。
struct X
{
bool operator==(const X&) const = default; // OK
bool operator==(const X&) = default; // 错误:隐式对象形参类型是 X&
bool operator==(this X, X) = default; // OK
};
struct Y
{
friend bool operator==(Y, Y) = default; // OK
friend bool operator==(Y, const Y&) = default; // 错误:形参类型不同
};
bool operator==(const Y&, const Y&) = default; // 错误:不是 Y 的友元
比較運算符函數的隱式定義中的名字查找和訪問檢查會在等價於該函數的函數體的語境中進行。在類中出現的將比較運算符函數預置的定義必須是該函數的首個聲明。
默認比較順序
給定類 C,按以下順序組成子對象列表:
C的直接基類子對象,按聲明順序。C的非靜態數據成員,按聲明順序。
- 如果有數組類型的成員,那麼它們會被展開成它們的元素序列,按下標升序。展開是遞歸的:類型也是數組的數組元素會被繼續展開,直到不存在數組類型的子對象。
對於類型 C 的任意對象 x,在後續描述中:
- 設
n為關於x的(擴展後的)子對象列表。 - 設
x_i為關於x的(擴展後的)子對象列表中的第i個子對象,其中x_i通過將派生類到基類轉換、類成員訪問表達式和數組下標表達式應用到x的序列組成。
struct S {};
struct T : S
{
int arr[2][2];
} t;
// “t” 的子对象列表按顺序包含以下 5 个子对象
// (S)t → t[0][0] → t[0][1] → t[1][0] → t[1][1]
三路比較
關於類類型的 operator<=> 可以以任意返回類型定義為預置。
比較類別類型
有以下三種比較類別類型:
| 類型 | 等價的值 | 無法比較的值 |
|---|---|---|
| std::strong_ordering | 不可以被區分 | 不允許比較 |
| std::weak_ordering | 可以被區分 | 不允許比較 |
| std::partial_ordering | 可以被區分 | 允許比較 |
合成三路比較
對於具有相同類型的泛左值 a 與 b 之間的 T 類型合成三路比較 定義如下:
- 如果對
a <=> b的重載決議產生了可用候選,並且可以通過static_cast顯式轉換到T,那麼合成比較是static_cast<T>(a <=> b)。 - 否則,如果滿足以下任意條件,那麼合成比較未定義:
- 對
a <=> b的重載決議找到了至少一個可行候選。 T不是比較類別類型。- 對
a == b的重載決議沒有產生可用候選。 - 對
a < b的重載決議沒有產生可用候選。
- 對
- 否則,如果
T是 std::strong_ordering,那麼合成比較是:
a == b ? std::strong_ordering::equal :
a < b ? std::strong_ordering::less :
std::strong_ordering::greater
- 否則,如果
T是 std::weak_ordering,那麼合成比較是:
a == b ? std::weak_ordering::equivalent :
a < b ? std::weak_ordering::less :
std::weak_ordering::greater
- 否則,如果
T是 std::partial_ordering),那麼合成比較是:
a == b ? std::partial_ordering::equivalent :
a < b ? std::partial_ordering::less :
b < a ? std::partial_ordering::greater :
std::partial_ordering::unordered
占位返回類型
如果預置的關於類類型 C 的三路比較運算符函數(operator<=>)聲明的返回類型是 auto,那麼返回類型會從 C 類型對象 x 的對應子對象的三路比較的返回類型推導。
對於關於 x 的(擴展後的)子對象列表中的每個子對象 x_i:
- 對
x_i <=> x_i進行重載決議,如果重載決議沒有產生可用候選,那麼預置的operator<=>會被定義為棄置。 - 以
R_i表示x_i <=> x_i的類型的無 cv 限定版本,如果R_i不是比較類別類型,那麼預置的operator<=>會被定義為棄置。
如果預置的 operator<=> 沒有被定義為棄置,那麼它的返回類型會被推導為 std::common_comparison_category_t<R_1, R_2, ..., R_n>。
非占位返回類型
如果預置的三路比較運算符函數(operator<=>)聲明的返回類型不是 auto,那麼它就不能包含任何占位類型(例如 decltype(auto))。
如果關於 x 的(擴展後的)子對象列表中存在子對象 x_i 使得 x_i 與 x_i 之間的聲明返回類型的合成三路比較未定義,那麼預置的 operator<=> 會被定義為棄置。
比較結果
設 x 和 y 為預置的 operator<=> 的形參,將 x 和 y 的(擴展後的)子對象列表的每個子對象分別記為 x_i 和 y_i。x 與 y 的默認三路比較會通過以 i 的升序依次比較對應的子對象 x_i 與 y_i。
設 R 為(可能經過推導的)返回類型,x_i 與 y_i 的比較結果是 x_i 與 y_i 之間的 R 類型的三路比較結果。
- 在進行
x與y的默認三路比較過程中,如果子對象x_i與y_i之間的比較產生了結果v_i使得將v_i != 0按語境轉換到bool會產生true,那麼返回值是v_i的副本(不會比較其餘子對象)。 - 否則,返回值是
static_cast<R>(std::strong_ordering::equal)。
#include <compare>
#include <iostream>
#include <set>
struct Point
{
int x;
int y;
auto operator<=>(const Point&) const = default;
/* 非比较函数 */
};
int main()
{
Point pt1{1, 1}, pt2{1, 2};
std::set<Point> s; // OK
s.insert(pt1); // OK
// 不需要显式定义双路比较运算符函数:
// operator== 会隐式声明(见下文)
// 而其他运算符的重载决议会选择重写候选
std::cout << std::boolalpha
<< (pt1 == pt2) << ' ' // false
<< (pt1 != pt2) << ' ' // true
<< (pt1 < pt2) << ' ' // true
<< (pt1 <= pt2) << ' ' // true
<< (pt1 > pt2) << ' ' // false
<< (pt1 >= pt2) << ' '; // false
}
相等比較
顯式聲明
關於類類型的 operator== 可以以 bool 返回類型定義為預置。
給定類 C 和 C 類型的對象 x,如果關於 x 的(擴展後的)子對象列表中存在子對象 x_i 使得對 x_i == x_i 的重載決議沒有產生可用候選,那麼預置的 operator== 會被定義為棄置。
設 x 和 y 為預置的 operator== 的形參,將 x 和 y 的(擴展後的)子對象列表的每個子對象分別記為 x_i 和 y_i。x 與 y 的默認相等比較會通過以 i 的升序依次比較對應的子對象 x_i 與 y_i。
x_i 與 y_i 的比較結果是 x_i == y_i 的結果。
- 在進行
x與y的默認相等比較過程中,如果子對象x_i與y_i之間的比較產生了結果v_i使得將v_i按語境轉換到bool會產生false,那麼返回值是false(不會比較其餘子對象)。 - 否則,返回值是
true。
#include <iostream>
struct Point
{
int x;
int y;
bool operator==(const Point&) const = default;
/* 非比较函数 */
};
int main()
{
Point pt1{3, 5}, pt2{2, 5};
std::cout << std::boolalpha
<< (pt1 != pt2) << '\n' // true
<< (pt1 == pt1) << '\n'; // true
struct [[maybe_unused]] { int x{}, y{}; } p, q;
// if (p == q) {} // 错误:operator== 未定义
}隱式聲明
如果類 C 沒有顯式聲明任何名為 operator== 的成員或友元,那麼對於每個定義為預置的 operator<=> 都會隱式聲明一個 運算符。每個隱式聲明的 operator== 都會與對應的預置 operator<=> 具有相同的訪問和函數定義,並且在相同的類作用域中,但有以下不同:
- 聲明符標識符會被替換成
operator==。 - 返回類型會被替換成
bool。
template<typename T>
struct X
{
friend constexpr std::partial_ordering operator<=>(X, X)
requires (sizeof(T) != 1) = default;
// 隐式声明:friend constexpr bool operator==(X, X)
// requires (sizeof(T) != 1) = default;
[[nodiscard]] virtual std::strong_ordering operator<=>(const X&) const = default;
// 隐式声明:[[nodiscard]] virtual bool operator==(const X&) const = default;
};次級比較
關於類類型的次級比較運算符函數(!=、<、>、<= 和 >=)可以以 bool 返回類型定義為預置。
設 @ 為五個次級比較運算符之一,對於每個形參是 x 和 y 的預置 operator@ 都會進行最多兩次重載決議(預置的 operator@ 不會被視為候選)以確定它是否會被定義為棄置。
- 第一次重載決議對
x @ y進行。如果重載決議沒有產生可用候選,或者選擇的候選不是重寫候選,那麼預置的operator@會被定義為棄置。這些情況下不會進行第二次重載決議。 - 第二次重載決議對
x @ y的重寫候選進行。如果重載決議沒有產生可用候選,那麼預置的operator@會被定義為棄置。
如果 x @ y 不能隱式轉換到 bool,那麼預置的 operator@ 會被定義為棄置。
如果預置的 operator@ 沒有被定義為棄置,那麼它會產生 x @ y.
struct HasNoRelational {};
struct C
{
friend HasNoRelational operator<=>(const C&, const C&);
bool operator<(const C&) const = default; // OK,函数被预置
};關鍵詞
缺陷報告
下列更改行為的缺陷報告追溯地應用於以前出版的 C++ 標準。
| 缺陷報告 | 應用於 | 出版時的行為 | 正確行為 |
|---|---|---|---|
| CWG 2539 | C++20 | 合成三路比較即使在無法進行顯式轉換時也會選擇 static_cast
|
此時不會選擇 static_cast
|
| CWG 2546 | C++20 | 對 x @ y 的重載決議選擇了不可用的重寫候選時 operator@ 不會被定義為棄置
|
此時會被定義為棄置 |
| CWG 2547 | C++20 | 不明確是否可以預置對非類的比較運算符函數 | 不可以預置 |
| CWG 2568 | C++20 | 比較運算符函數的隱式定義可能會違反成員訪問規則 | 會在等價於它們的函數體 的語境中進行訪問檢查 |