集合(set,map等)算法通常会涉及元素比大小,因此两个元素(类型)的比较操作就显得额外重要。两个类型之间的共有六种比较符(<,<=,>,>=,==,!=),如果需要完整的表达,就要写六个函数,如果需要与可转换类型比较,这项工作显得比较繁琐,因此这也是C++引入<=>的基本初衷。【如果认为用一个比较符,例如”<”就可以推导出另外几个,其实这只适用于一些场景,这也是C++引入新的比较操作符的另一个原因。】
<=>称为3-way comparison operator(三路比较符),它的定义如下:
a <=> b 返回一个类型,该类型在a < b,,a > b,a等效b三种情况下与0的比较分别是小于0,大于0,等于0。
上面的0仅是一个字面量,最好不要理解成int 0。按照其定义,原来两个类型之间的六种比较符(<,<=,>,>=,==,!=),都可采用<=>来表达,即:
a@b可表达为a<=>b @ 0或 0 @ b<=>a
其中@就是上面六种比较符,例如a<b可表达为a<=>b <0或 0 < b<=>a(编译器会尝试前者,不成功再尝试后者)。
上面定义中,<=>的返回值是一个对象(模板类),它的类型可以是:
- std::strong_ordering
- std::weak_ordering
- std::partial_ordering
为理解它们的意义,先说明一下如果对一个集合中的元素进行排序,可能出现的情况:
- 完全且严格顺序:所有元素都可比较,且有严格的顺序关系(可比较,等值唯一)
- 完全且松散顺序:所有元素都可比较,但存在近似相等的情况(可比较,等值不唯一)
- 不完全顺序:元素存在不可比较的情况(非全可比较,等值也不唯一)
如整数集合属于第一种情况,浮点数集合、大小写不区分的字符集合属于第二种情况,包含空值的字符集、包含NaN的数值属于第三种情况,很明显,这三种情况就分别对应std::strong_ordering,std::weak_ordering和std::partial_ordering。
标准库已定义了若干上述类型的静态对象,例如std::partial_ordering类型定义了4个:
- std::partial_ordering::less
- std::partial_orderingequivalent
- std::partial_ordering::greater
- std::partial_ordering::unordered
而std::strong_ordering定义了4个,std::weak_ordering定义了3个。这些静态对象就是操作符<=>的返回值,它们与字面量0的操作示例如下,结果与以前对<=>的定义是一致的,需要特别注意的是std::partial_ordering::unordered与0的比较都是false。
strong_ordering::less < 0 // true
strong_ordering::less == 0 // false
strong_ordering::less != 0 // true
strong_ordering::greater >= 0 // true
// unordered is a special value that isn't comparable against anything
partial_ordering::unordered < 0 // false
partial_ordering::unordered == 0 // false
partial_ordering::unordered > 0 // false
可以想象,strong_ordering类型是可以转换为相应的weak_ordering或partial_ordering类型,但反过来却不行。
在重载(自定义)类型的<=>时,会用到这些静态对象。
<=>应用的一个基本例子如下,其中三种类型的比较都应用了系统缺省的<=>实现(尽量利用系统缺省实现,这也是推荐做法),输出都是1(true)。
void test_compare()
{
struct ts {
int i;
char c;
float f;
auto operator<=>(const ts&) const = default;
};
int ia=1, ib=2;
auto cmp1=ia<=>ib;
std::cout<<(cmp1<0)<<std::endl;
std::vector<int> va={1,2,3};
std::vector<int> vb={2,3};
auto cmp2=va<=>vb;
std::cout<<(cmp2<0)<<std::endl;
ts ta = { 0, 'c', 1.0f};
ts tb={ 0, 'c', 1.0f};
std::cout<< (ta==tb) <<std::endl;
}
下面是一个我们重载<=>的一个示例。
void test_compare()
{
struct ts {
int id;
float f;
std::partial_ordering operator<=>(const ts& that) const
{
if(id<0 || that.id<0) return std::partial_ordering::unordered;
if(f>that.f) return std::partial_ordering::greater;
if(f<that.f) return std::partial_ordering::less;
return std::partial_ordering::equivalent;
}
bool operator==(const ts& that) const
{
return (*this<=>that)==0;
}
};
ts ta = {2, 1.0f};
ts tb={3, 1.0f};
std::cout<< (ta==tb) <<std::endl;
std::cout<< (ta>tb) <<std::endl;
}
上面示例中,只有在id大于0时才进行比较。当然这个示例没有什么实际意义。要注意的是,目前重载<=>后,>=,>,<=,<可自动调用<=>并做相应转换,==,!=还需单独编写==,如上例所示,如果去掉==重载,则ta==tb不能编译通过。因此,有下表的关系(来自于https://brevzin.github.io/c++/2019/07/28/comparisons-cpp20):
| Equality | Ordering |
Primary | == | <=> |
Secondary | != | <, >, <=, >= |
其中,Secondary行中的比较符可由对应primary行中的比较符隐式转换而来,也就是一个新类型的六种比较关系现在由两种比较来完成。