|
我们在项目中经常需要将一些struct的值用作set/map的key,所以就需要实现自定义的小于号运算符。这个运算如果实现得不对(可能会出现自相矛盾,a < b 且 b < a),在set/map中排序也是不对的,而且程序可能会崩溃。但是根据我碰到的实际情况,这个功能对新手来说并不简单。举个例子吧:
struct Name {
string first_name;
string mid_name;
string last_name;
};
如果要实现这个Name的operator<,因为有3个成员,所以有些人会用一些嵌套的if语句,如果有更多成员呢?看起来就很复杂很容易出错了。我的要求是写成下面这样,这也是我每次code review的时候会去检查的一个功能点:
bool operator<(const Name& other) const {
//before c++11
return first_name<other.first_name
|| first_name == other.first_name && mid_name < other.mid_name
|| first_name == other.first_name && mid_name == other.mid_name && last_name < other.last_name;
//after c++11
return std::tie(first_name, mid_name, last_name) <
std::tie(other.first_name, other.mid_name, other.last_name);
}
无论哪种写法,都比自己整一些if else要简单得多,但是在C++20中,这种应用有更简单的方法,只需要一句:
std::strong_ordering operator<=>(const Name&) const = default;
只要这一句,编译器会帮你实现绝对正确的operator<,同时还帮你实现了==, !=, >, >=, <=这几个运算符。
<=>在C++20中叫做三向比较(three way comparation),它提出了一些新的概念,包括强有序、弱有序、强相等、弱相等(strong ordering/weak ordering/strong equality/weak equality),如下图所示:

肯定有的人会说哎呀怎么又变复杂了。其实three-way comparation并不是C++的发明,某些语言在更早的时候就有了,C++不过是跟进而已。而选用<=>来作为C++的三向比较运算符也不是C++独有,至少还有PHP也选用了它作为三向比较。
其实<=>有一个很高级的名字,叫做飞船运算符(spaceship operator),因为它很像星球大战中的某一款飞船:

operator<=>的返回值不是int,而是定义成一些enum,像我上面举的那个Name的例子,返回值类型是strong_ordering,全部可能的返回值类型和其enum对应的值如下:

本篇是入门篇,我只介绍strong_ordering和weak_ordering,它们有何区别呢?对于某个类型的对象,如果你想“严格比较”它们的”全部成员”,则用strong_ordering,否则用weak_ordering。有两个条件,一个是“严格比较”,像我前面实现的Name,对每个字符进行比较,所以我选择用strong_ordering,如果我想进行不区分大小写的比较,则返回类型应该选用weak_ordering;第二个条件是“全部成员”,我对Name的3个成员first_name/mid_name/last_name三个都纳入了比较的范围,所以是strong_ordering,如果只比较其中的一个或是两个,就用weak_ordering。
如果要将全部的成员纳入比较范围,则只要用default,具体代码会由编译器帮你实现。但是如果只比较部分,就要自己实现呢。如何自己实现operator<=>呢?我再举个例子。
struct ID {
int id_number;
auto operator<=>(const ID&) const = default;
};
struct Person {
ID id;
string name;
string email;
std::weak_ordering operator<=>(const Person& other) const
{
return id<=>other.id;
}
};
这个例子中的Person因为只要根据id来比较,所以返回的是weak_ordering,它可以通过调用string ID的operator<=>来实现。
前面说了,operator<=>的代码可以由编译器来生成,但是有一个注意事项。就是类成员中有容器类型(例如vector)时,需要将operator==单独列出来,像这样:
struct SomeType {
int int_property;
std::vector<int> some_ints; // vector是容器
std::strong_ordering operator<=>(const SomeType&) const = default;
bool operator==(const SomeType&) const = default; // 加上这一行
};
这是为何呢?这是为了性能考虑的。编译器生成的operator==的代码,对于容器类型会先比较容器的size,如果size不同,则必定不等,size相等再去逐个比较内容,这样实现效率高,但是operator<=>要同时实现几个运算符的逻辑,它生成的代码是从头比到尾,对于容器也是如此,并不会先去比size,所以在有容器成员的时候它生成的operator==代码性能不是最高,这时候需要单独将operator==再明确写一次。
C++20的operator<=>大大地节省了程序员的时间,对于很多类,以前可能需要实现十几二十几运算符的,现在只需要几个了。
operator<=>对于语言本身也有改进。C++20以前的比较运算符定义有两种方法,但是分别都有缺点。第一种是通过成员函数,假设有一个封装的Str类,是这样定义的:
bool Str::operator==(const char*) const {...}
这样就可用if(s == &#34;xyz&#34;)了,但是if(&#34;xyz&#34; == s)却编不过, 需要作为firend函数定义两次
friend bool operator==(const Str&, const char*) const {...}
friend bool operator==(const char*, const Str&) const {...}不仅仅是对于==,对于其他<, >, !=, >=, <=同样需要实现两次,所以每个类与另一个不同类型的对象比较需要实现12个类似的函数。但是operator<=>就没这么麻烦,一次搞定。
第二种就是上面提到的friend函数,像这样定义:
friend bool operator==(const Str&, const Str&) {...}
这样就可以用if(s == &#34;xyz&#34;)和if(&#34;xyz&#34; == s)了,但是又会有更严重的缺陷,假设某个类X可以转化为Str,则if(x1 == x2)也能编过,即使类X根本没有operator==,编译器会来找Str的operator==,这简直是一个bug。但是operator<=>不允许两个参数同时隐式转换。所以说operator<=>比以前的比较运算符定义方式肯定是要好。 |
|