spaceship operator

六月 11, 2022 [c++, c++20] #c++ #c++20

spaceship operator

operator<=> for C++20

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);
}

// c++20
std::strong_ordering operator<=>(const Name&) const = default;

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

image

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。

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;
    }
};

类成员中有容器类型时

前面说了,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<=>对于语言的改进

operator<=>对于语言本身也有改进。C++20以前的比较运算符定义有两种方法,但是分别都有缺点。第一种是通过成员函数,假设有一个封装的Str类,是这样定义的:

bool Str::operator==(const char*) const {...}

这样就可用if(s == "xyz")了,但是if("xyz" == 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 == "xyz")和if("xyz" == s)了,但是又会有更严重的缺陷,假设某个类X可以转化为Str,则if(x1 == x2)也能编过,即使类X根本没有operator==,编译器会来找Str的operator==,这简直是一个bug。但是operator<=>不允许两个参数同时隐式转换。所以说operator<=>比以前的比较运算符定义方式肯定是要好。

compare ordering

+--------------------------------------------------------------------+
|                  |          Numeric  values          | Non-numeric |
|     Category     +-----------------------------------+             |
|                  | -1   | 0          | +1            |   values    |
+------------------+------+------------+---------------+-------------+
| strong_ordering  | less | equal      | greater       |             |
| weak_ordering    | less | equivalent | greater       |             |
| partial_ordering | less | equivalent | greater       | unordered   |
| strong_equality  |      | equal      | nonequal      |             |
| weak_equality    |      | equivalent | nonequivalent |             |
+------------------+------+------------+---------------+-------------+

比如a与b是整型,那么a<=>b返回std::strong_ordering类型的纯右值(prvalue):

如果a<b,(a<=>b)返回std::strong_ordering::less 如果a>b,(a<=>b)返回std::strong_ordering::greater 如果a与b全等/相等,(a<=>b)返回std::strong_ordering::equal

如果a b是浮点值,就返回的是std::partial_ordering::less/greater/equivalent或者std::partial_ordering::unordered(如果ab中有一个NaN)

标准还有更多规定,比如a与b是指针,a与b是枚举等