默认比较(C++20 起)

来自cppreference.com
< cpp‎ | language
 
 
 
表达式
概述
值类别(左值 lvalue、右值 rvalue、亡值 xvalue)
求值顺序(序列点)
常量表达式
潜在求值表达式
初等表达式
lambda 表达式(C++11)
字面量
整数字面量
浮点字面量
布尔字面量
字符字面量,包含转义序列
字符串字面量
空指针字面量(C++11)
用户定义字面量(C++11)
运算符
赋值运算符a=ba+=ba-=ba*=ba/=ba%=ba&=ba|=ba^=ba<<=ba>>=b
自增与自减++a--aa++a--
算术运算符+a-aa+ba-ba*ba/ba%b~aa&ba|ba^ba<<ba>>b
逻辑运算符a||ba&&b!a
比较运算符a==ba!=ba<ba>ba<=ba>=ba<=>b(C++20)
成员访问运算符a[b]*a&aa->ba.ba->*ba.*b
其他运算符a(...)a,ba?b:c
new 表达式
delete 表达式
throw 表达式
alignof
sizeof
sizeof...(C++11)
typeid
noexcept(C++11)
折叠表达式(C++17)
运算符的代用表示
优先级和结合性
运算符重载
默认比较(C++20)
类型转换
隐式转换
一般算术转换
const_cast
static_cast
reinterpret_cast
dynamic_cast
显式转换 (T)a, T(a)
用户定义转换
 

提供一种方式,以要求编译器为某个类生成相一致的比较运算符。

语法

返回类型 类名 ::operator运算符
    (const 类名 &) const &(可选) = default;
(1) (C++20 起)
friend 返回类型 operator运算符 (const 类名 &, const 类名 &) = default; (2) (C++20 起)
friend 返回类型 operator运算符 (类名 , 类名 ) = default; (3) (C++20 起)
返回类型 类名 ::operator运算符
    (this const 类名 &, const 类名 &) = default;
(4) (C++23 起)
返回类型 类名 ::operator运算符 (this 类名 , 类名 ) = default; (5) (C++23 起)
运算符 - 比较运算符(<=>==!=<><=,或 >=
返回类型 - 运算符函数的返回类型

解释

1) 将默认比较函数声明为成员函数。
2) 将默认比较函数声明为非成员函数。
3) 将默认比较函数声明为非成员函数。参数按值传递。

每当有值通过 <><=>=,或 <=> 被比较且重载决议选择该重载时,三路比较函数(不管是否为默认)会被调用。

每当有值通过 ==!= 被比较且重载决议选择该重载时,相等比较函数(不管是否为默认)会被调用。

与默认的特殊成员函数类似,默认的比较函数在被 ODR 使用被常量求值所需时被定义。

默认比较

默认三路比较

默认的 operator<=> 通过依次以计算 <=> 比较基类(从左到右,深度优先),然后是非静态成员(按声明顺序)子对象,递归地展开数组成员(按下标递增),并在发现不相等的结果时提前停止的方式执行字典序比较,即:

for /* T 的每个基类或子对象 o */
    if (auto cmp = static_cast<R>(compare(lhs.o, rhs.o)); cmp != 0)
        return cmp;
return static_cast<R>(strong_ordering::equal);

虚基子对象是否会多次被比较未被指明。

如果被声明的返回类型是 auto,实际的返回类型是要被比较的基类,成员子对象和成员数组元素的公共比较类别(见 std::common_comparison_category)。这样在编写返回类型非平凡地依赖于成员的场合时会更容易,例如:

template<class T1, class T2>
struct P
{
    T1 x1;
    T2 x2;
    friend auto operator<=>(const P&, const P&) = default;
};

将返回类型设为 R,每一对子对象 ab 按如下方法进行比较:

  • 如果 a <=> b 可用且可以通过 static_cast 显式转换到 R,那么比较结果是 static_cast<R>(a <=> b)
  • 否则,如果对 a <=> b 执行重载决议且至少找到一个候选,那么比较未定义(即 operator<=> 被定义为弃置的)。
  • 否则,如果 R 不是比较类别之一(见下文)或 a == ba < b 其中有一个不可用,那么比较未定义(即 operator<=> 被定义为弃置的)。
  • 否则,如果 Rstd::strong_ordering,那么结果是
a == b ? R::equal :
a < b  ? R::less :
         R::greater
a == b ? R::equivalent :
a < b  ? R::less :
         R::greater
a == b ? R::equivalent :
a < b  ? R::less :
b < a  ? R::greater : 
         R::unordered

与任何 operator<=> 重载的规则一样,默认的 <=> 重载也允许类型被 <><=,和 >= 比较。

如果 operator<=> 是默认版本且 operator== 完全没有被声明,那么 operator== 将隐式地采用默认版本。

#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
 
    std::cout << std::boolalpha
        << (pt1 == pt2) << ' '  // false; operator== 隐式地采用默认版本
        << (pt1 != pt2) << ' '  // true
        << (pt1 <  pt2) << ' '  // true
        << (pt1 <= pt2) << ' '  // true
        << (pt1 >  pt2) << ' '  // false
        << (pt1 >= pt2) << ' '; // false
}

默认相等比较

类可以定义 operator== 为默认版本,它返回一个 bool 值。这会以声明顺序对每个基类和成员子对象生成一轮相等比较。两个对象在它们每个基类和成员相等时相等。该检测会以声明顺序在找到基类或成员里出现不相等的情况下短路。

与任何 operator== 重载的规则一样,不等测试也能被允许:

#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==' 未被定义
}

其他默认比较操作符

四个关系运算符(<><=>=)均可以显式指定为默认版本。默认的关系运算符必须返回 bool

如果 x <=> y 的重载决议(包括参数交换后的 operator<=>)失败,或 operator@ 无法被应用到 x <=> y 的结果,该操作符是弃置的。否则,默认的 operator@ 在重载决议选择参数顺序不变的 operator<=> 时会调用 x <=> y @ 0,否则会调用 0 @ y <=> x

struct HasNoRelational {};
 
struct C
{
    friend HasNoRelational operator<=>(const C&, const C&);
    bool operator<(const C&) = default; // OK:函数被弃置
};

与此类似,operator!= 也可以显式指定为默认版本。如果 x <=> y 的重载决议失败,或 x == y 结果的类型不是 bool,它也会被弃置。默认的 operator!= 会根据重载决议的选择调用 !(x == y) 或者 !(y == x)

指定关系运算符使用默认版本可用于创建可取址的函数。其他场合仅需提供 operator<=>operator==

自定义的比较和比较类别

在默认语义不适用的情况下,例如成员不能按顺序比较,或者不能采用自然比较,那么程序员可以自定义 operator<=> 并让编译器生成合适的比较运算符。比较运算符的种类由用户定义的 operator<=> 决定。

返回类型有三种:

返回类型 运算符 等价的值 无法比较的值
std::strong_ordering == != < > <= >= 不可以被区分 不允许比较
std::weak_ordering == != < > <= >= 可以被区分 不允许比较
std::partial_ordering == != < > <= >= 可以被区分 允许比较

强序

在这个自定义 operator<=> 返回 std::strong_ordering 的例子中,该操作符比较了类的每个成员,只是顺序不同(在这里名在姓前面比较)。

#include <cassert>
#include <compare>
#include <set>
#include <string>
 
struct Base
{
    std::string zip;
    auto operator<=>(const Base&) const = default;
};
 
struct TotallyOrdered : Base
{
    std::string tax_id;
    std::string first_name;
    std::string last_name;
public:
    // 自定义 operator<=> ,因为我们希望(在比较姓前)先比较名:
    std::strong_ordering operator<=>(const TotallyOrdered& that) const
    {
        if (auto cmp = (Base&)(*this) <=> (Base&)that; cmp != 0)
            return cmp;
        if (auto cmp = last_name <=> that.last_name; cmp != 0)
            return cmp;
        if (auto cmp = first_name <=> that.first_name; cmp != 0)
            return cmp;
        return tax_id <=> that.tax_id;
    }
    // ... 非比较函数 ...
};
// 编译器生成全部四个关系运算符
 
int main()
{
    TotallyOrdered to1{"a", "b", "c", "d"}, to2{"a", "b", "d", "c"};
    std::set<TotallyOrdered> s; // OK
    s.insert(to1); // OK
    assert(to2 <= to1); // OK,调用 <=> 一次
}

注:返回 std::strong_ordering 的操作符需要比较每个成员,因为如果有成员没有被比较,可替换性会受牵连:两个可被区分的值有可能会比较相等。

弱序

在这个自定义 operator<=> 返回 std::weak_ordering 的例子中,该操作符不分大小写地比较了类的字符串成员:这和默认比较不同(因此需要自定义运算符)且通过这种方式比较相等的两个字符串可能可以被区分。

class CaseInsensitiveString
{
    std::string s;
public:
    std::weak_ordering operator<=>(const CaseInsensitiveString& b) const
    {
        return case_insensitive_compare(s.c_str(), b.s.c_str());
    }
 
    std::weak_ordering operator<=>(const char* b) const
    {
        return case_insensitive_compare(s.c_str(), b);
    }
    // ... 非比较函数 ...
};
 
// 编译器生成全部四个关系运算符
CaseInsensitiveString cis1, cis2;
std::set<CaseInsensitiveString> s; // OK
s.insert(/*...*/); // OK
if (cis1 <= cis2) { /*...*/ } // OK,执行一次比较操作
 
// 编译器也生成了全部八个混合参数的关系运算符
if (cis1 <= "xyzzy") { /*...*/ } // OK,执行一次比较操作
if ("xyzzy" >= cis1) { /*...*/ } // OK,语义相同

注:这个例子展示了参数类型不同的 operator<=> 的效果:它生成了双向的不同类型参数的比较。

偏序

偏序是一种允许无法比较(无序)的值的比较的排序,比如包括 NaN 值的浮点排序,或在这个例子里的没有关联的人:

class PersonInFamilyTree // ...
{
public:
    std::partial_ordering operator<=>(const PersonInFamilyTree& that) const
    {
        if (this->is_the_same_person_as(that))
            return partial_ordering::equivalent;
        if (this->is_transitive_child_of(that))
            return partial_ordering::less;
        if (that. is_transitive_child_of(*this))
            return partial_ordering::greater;
        return partial_ordering::unordered;
    }
    // ... 非比较函数 ...
};
// 编译器生成全部四个关系运算符
 
PersonInFamilyTree per1, per2;
if (per1 < per2) { /*...*/ } // OK,per2 是 per1 的先人
else if (per1 > per2) { /*...*/ } // OK,per1 是 per2 的先人
else if (std::is_eq(per1 <=> per2)) { /*...*/ } // OK,per1 是 per2(同一人)
else { /*...*/ } // per1 与 per2 没有(直系)关联
 
if (per1 <= per2) { /*...*/ } // OK,per2 是 per1 或 per1 的先人
if (per1 >= per2) { /*...*/ } // OK,per1 是 per2 或 per2 的先人
if (std::is_neq(per1 <=> per2)) { /*...*/ } // OK,per1 不是 per2(但可能有直系关联)

参阅