Marvin's Blog【程式人生】

Ability will never catch up with the demand for it

28 Jun 2020

C++追踪【二】飞船运算符

编译器在编译的时候其实是知道每个值的类型,有办法生成对于每个值的比较操作。尤其是对于整型值这些天然存在有次序的值,只要按照次序进行比较即可。对于像浮点值或者字符串值会比较复制一点,浮点值有可能是NaN,从而无法进行比较;字符串对比时则需要考虑其长度。

原本C++对于简单的自定义类型是可以生成比较操作的,也就是把自定义类型中的每个成员拿出来,逐个进行对比。这种按所有成员进行比较的方式属于强次序性,也就是std::strong_ordering描述的情况。

C++20通过提供飞船运算符,来帮助编译器能够在更复杂的情况下自动生成代码。

下面的两个章节是对网上两篇文章的学习笔记。

Simplify Your Code With Rocket Science: C++20’s Spaceship Operator

C++ 20引入了一个飞船运算符(也叫海象运算符):<=>,可以帮你处理比较相关的操作,不用重载很多比较运算符(比如 ==, !=, <, >,等等)。一个例子:

#include <compare>
struct IntWrapper {
  int value;
  constexpr IntWrapper(int value): value{value} { }
  auto operator<=>(const IntWrapper&) const = default;
};

你或许已经注意到了,首先要包含<compare>头文件,其次要声明海象运算符,但是可以指定为default,以便让编译器自动生成代码。你或许可能注意不到,海象运算符返回的auto,其实会被推导成为std::strong_ordering.

C++20引入了Rewritten Expression的概念,简单地说是允许编译器修改用户代码,但是目前仅限于operator <=>operator ==

当编译器遇到表达式a < b的时候,会去查找a是否有operator <=>,如果没有,那么会在命名空间范围内查找能够接受a的operator <=>。如果找到了,那么编译器被允许把a < b改写成(a <=> b) < 0

飞船运算符返回的不是真假类型,而是一个ordering。对于 4 <=> 5来说,C++20会返回std::strong_ordering::less,用于表明4在次序上比5小,也就是(4 <=> 5) < 0能够成立。

感觉飞船运算符是C语言的strcmp在C++中的化身,只不过strcmp只能接受指针,而飞船运算符可以接受任意类型。

另外对于表达式42 < a,编译器不仅会尝试改写成:(42 <=> a) < 0,也会尝试改写成:0 < (a <=> 42)。达到相互转化的效果。

当一个类具有了飞船操作符之后,编译器也会考虑其所有的子类型的ordering。

飞船运算符会让编译器生成许多代码,这不一定是最优化的选择。比如比较字符串相等,可以先判断字符串的长度是否相等,再深入其中判读内容是否相等。飞船运算符默认的语义是深度对比,也就是跳过判读字符串长度,直接对其内容进行判断。可以选择让编译器来自动生成对等运算符:

#include <compare>
struct IntWrapper {
  int value;
  constexpr IntWrapper(int value): value{value} { }
  auto operator<=>(const IntWrapper&) const = default;
  bool operator==(const IntWrapper&) const = default;
};

默认的operator==的存在不仅会影响== 还会影响!=操作符。

另外要说的是,即便使用到了默认的飞船运算符以及相等运算符,自定义的比较运算符依然会被编译器优先选中。

Default comparisons (since C++20)

C++20定义了五种比较语义:

  • std::strong_ordering,覆盖操作符==, !=, <, >, <=, >=,对等的值必须整体相同
  • std::weak_ordering,覆盖操作符==, !=, <, >, <=, >=,对等的值可以部分不同
  • std::partial_ordering,覆盖操作符==, !=, <, >, <=, >=,判断对等的时候,可以允许不可比较的值
  • std::strong_equality,覆盖操作符==, !=,对等的值必须整体相同
  • std::weak_ordering,覆盖操作符==, !=,对等的值可以部分不同

std::strong_ordering可以看出是一致性要求最严格,并且操作符覆盖最广。 std::weak_ordering在一致性上比std::strong_ordering要弱一些,一个例子是大小写不同的字符串也可以看出是对等的。 std::partial_ordering可以允许用户自定义的比较,来返回那些不能直接比较的值的比较结果。

有些类型的值可以判断是否对等,但是不可以比大小,可以用std::strong_equality(std::strong_ordering的删减版)以及 std::weak_ordering(std::weak_ordering)的删减版来适配。

即便定义了三元的飞船运算符来生成所有的比较运算符,你依旧可以定义某些比较操作符。如果这些比较运算符被设成是默认的,那么会被删除(可能其存在的意义是为了方便取地址操作);如果这些比较运算符不是默认的,那么会被优先使用。

其他

(完)

comments powered by Disqus