IE盒子

搜索
查看: 187|回复: 21

operator<=> for C++20入门篇

[复制链接]

3

主题

7

帖子

10

积分

新手上路

Rank: 1

积分
10
发表于 2022-12-21 16:48:01 | 显示全部楼层 |阅读模式
我们在项目中经常需要将一些struct的值用作set/map的key,所以就需要实现自定义的小于号运算符。这个运算如果实现得不对(可能会出现自相矛盾,a &lt; b 且 b &lt; a),在set/map中排序也是不对的,而且程序可能会崩溃。但是根据我碰到的实际情况,这个功能对新手来说并不简单。举个例子吧:
struct Name {
  string first_name;
  string mid_name;
  string last_name;
};
如果要实现这个Name的operator&lt;,因为有3个成员,所以有些人会用一些嵌套的if语句,如果有更多成员呢?看起来就很复杂很容易出错了。我的要求是写成下面这样,这也是我每次code review的时候会去检查的一个功能点:
bool operator&lt;(const Name&amp; other) const {
  //before c++11
  return first_name&lt;other.first_name
      || first_name == other.first_name &amp;&amp; mid_name &lt; other.mid_name
      || first_name == other.first_name &amp;&amp; mid_name == other.mid_name &amp;&amp; last_name &lt; other.last_name;

  //after c++11
  return std::tie(first_name, mid_name, last_name) &lt;
      std::tie(other.first_name, other.mid_name, other.last_name);
}
无论哪种写法,都比自己整一些if else要简单得多,但是在C++20中,这种应用有更简单的方法,只需要一句:
std::strong_ordering operator&lt;=&gt;(const Name&amp;) const = default;
只要这一句,编译器会帮你实现绝对正确的operator&lt;,同时还帮你实现了==, !=, &gt;, &gt;=, &lt;=这几个运算符。
&lt;=&gt;在C++20中叫做三向比较(three way comparation),它提出了一些新的概念,包括强有序、弱有序、强相等、弱相等(strong ordering/weak ordering/strong equality/weak equality),如下图所示:


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


operator&lt;=&gt;的返回值不是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&lt;=&gt;呢?我再举个例子。
struct ID {
    int id_number;
    auto operator&lt;=&gt;(const ID&amp;) const = default;
};

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

使用道具 举报

1

主题

3

帖子

5

积分

新手上路

Rank: 1

积分
5
发表于 2022-12-21 16:48:15 | 显示全部楼层
手动赞一个
回复

使用道具 举报

3

主题

11

帖子

19

积分

新手上路

Rank: 1

积分
19
发表于 2022-12-21 16:48:57 | 显示全部楼层
看起来不错
回复

使用道具 举报

2

主题

12

帖子

19

积分

新手上路

Rank: 1

积分
19
发表于 2022-12-21 16:49:47 | 显示全部楼层
不错啊
回复

使用道具 举报

1

主题

9

帖子

13

积分

新手上路

Rank: 1

积分
13
发表于 2022-12-21 16:50:08 | 显示全部楼层
惭愧,c11都没弄太明白,这都c20了
回复

使用道具 举报

2

主题

7

帖子

11

积分

新手上路

Rank: 1

积分
11
发表于 2022-12-21 16:50:28 | 显示全部楼层
谢谢,写的不错。
回复

使用道具 举报

1

主题

6

帖子

3

积分

新手上路

Rank: 1

积分
3
发表于 2022-12-21 16:50:39 | 显示全部楼层
spaceship
回复

使用道具 举报

1

主题

3

帖子

4

积分

新手上路

Rank: 1

积分
4
发表于 2022-12-21 16:50:50 | 显示全部楼层
妙啊
回复

使用道具 举报

1

主题

2

帖子

4

积分

新手上路

Rank: 1

积分
4
发表于 2022-12-21 16:51:28 | 显示全部楼层
爽翻了
回复

使用道具 举报

1

主题

7

帖子

12

积分

新手上路

Rank: 1

积分
12
发表于 2022-12-21 16:51:52 | 显示全部楼层
初一看C++又又又更复杂了,仔细想想当遇到这样的需求时C++又帮你把语法准备好了
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

快速回复 返回顶部 返回列表