C++20 三向比较
老兵
作为一名在战场上出生入死多年的老兵,对于手中的武器C++,我有充分的理由相信自己已经对她身上的每一寸肌肤都了如指掌,直到有一天,我被下面的代码吓了一跳:
struct Num { int a; long b; Num(int a_ = 0, long b_ = 0) :a(a_), b(b_) { } auto operator <=> (const Num&) const = default; };
这是个啥?<=>是什么?这还是你最熟悉的那个她吗?于是,我赶紧打开最新的C++标准手册。
很快,我就发现,这玩意真的很扯淡,增加了一个运算符,就是为了简化一下关系运算符的实现?
我快速的打开了自己视若性命的《C++保命手册》,快速翻到实现关系运算符的部分:
struct Rational { int numeratorand; int denominator; Rational(int numeratorand_ = 0, int denominator_ = 1) :numeratorand(numeratorand_), denominator(denominator_) { } bool operator<(const Rational& other) const { return numeratorand * other.denominator - other.numeratorand * denominator < 0; } bool operator==(const Rational& other) const { return numeratorand * other.denominator - other.numeratorand * denominator == 0; } inline int compare(const Rational& other) const { return numeratorand * other.denominator - other.numeratorand * denominator; } }; TEST_METHOD(TestRational) { Rational a(1, 2); Rational b(2, 3); Assert::IsTrue(a < b); Assert::IsTrue(b > a); Assert::IsTrue(a <= b); Assert::IsTrue(b >= a); Assert::IsTrue(a == a); Assert::IsTrue(a != b); Assert::IsTrue(a.compare(b) < 0); Assert::IsTrue(a.compare(a) == 0); Assert::IsTrue(b.compare(a) > 0); }
我长舒了一口气,这才是C++嘛,这才是自定义类型关系运算符功能的标准实现嘛。其中我最引以为傲,不断向新兵们炫耀的两大秘籍是:
1、只需要实现operator<和operator==这2个关系运算符,剩下的4个关系运算符可以借助于std::rel_ops空间中的模板自动推导出来
2、实现六种关系运算符会使你的代码看起来漂亮很多(因为运算符重载),但是那些从C语言时代过来的更老练的老兵们其实更喜欢实现compare,真的是一句话搞定所有问题。
当然,上面的代码还可以精简一下,可以通过调用compare来实现那2个关系运算符。
新兵
新兵对老兵的所谓秘籍是不屑一顾的,他们直接搬出来下面的代码:
TEST_METHOD(TestInt) { int a = 1; int b = 2; Assert::IsTrue(a <=> b == std::strong_ordering::less); Assert::IsTrue(a <=> a == std::strong_ordering::equal); Assert::IsTrue(b <=> a == std::strong_ordering::greater); Assert::IsTrue((a <=> b) < 0); Assert::IsTrue((a <=> a) == 0); Assert::IsTrue((b <=> a) > 0); Assert::IsTrue((a <=> b) < nullptr); Assert::IsTrue((a <=> a) == nullptr); Assert::IsTrue((b <=> a) > nullptr); }
对于int类型来说,<=>运算符可以返回3种结果,从理论上说,新标准对比较这个功能做了更为严谨的划分,int类型是可以强顺序比较的(std::strong_ordering),两个量一旦相等同时也就意味着两个量可以互换。
至于老兵们喜欢的compare,通过引入std::strong_ordering和常量0(或nullptr)的比较,新的语法可以很好的模拟compare的语法。
当然了,有std::strong_ordering就意味着还有非std::strong_ordering的:
TEST_METHOD(TestDouble) { double a = 1; double b = 2; bool is = 0.0 == -0.0; Assert::IsTrue(a <=> b == std::weak_ordering::less); Assert::IsTrue(a <=> a == std::weak_ordering::equivalent); Assert::IsTrue(b <=> a == std::weak_ordering::greater); Assert::IsTrue((a <=> b) < 0); Assert::IsTrue((a <=> a) == 0); Assert::IsTrue((b <=> a) > 0); Assert::IsTrue((a <=> b) < nullptr); Assert::IsTrue((a <=> a) == nullptr); Assert::IsTrue((b <=> a) > nullptr); }
因为浮点数有精度问题,即使相等也不一定可以互换,所以它是弱顺序比较。
还有其它类型的比较,不过都大同小异,就不多说了。
对于新兵来说,真正关键的是,<=>运算符的默认实现是全自动的:
- 会替你比较自定义类型中每一个成员变量
- 会替你处理基类、子对象等这些细节
- 会替你自动完成关系运算符的实现
struct Num { int a; long b; Num(int a_ = 0, long b_ = 0) :a(a_), b(b_) { } auto operator <=> (const Num&) const = default; }; TEST_METHOD(TestNum) { Num a(1, 1); Num b(1, 2); Assert::IsTrue(a == a); Assert::IsTrue(a != b); Assert::IsTrue(a <=> b == std::weak_ordering::less); Assert::IsTrue(a <=> a == std::weak_ordering::equivalent); Assert::IsTrue(b <=> a == std::weak_ordering::greater); Assert::IsTrue((a <=> b) < 0); Assert::IsTrue((a <=> a) == 0); Assert::IsTrue((b <=> a) > 0); Assert::IsTrue(a < b); Assert::IsTrue(b > a); Assert::IsTrue(a <= b); Assert::IsTrue(b >= a); Assert::IsTrue(a == a); Assert::IsTrue(a != b); }
你看,我们的Num类几乎啥也没干,就是招呼了一下<=>的默认实现,它就替我们搞定了所有的事情。
“会替你自动完成关系运算符的实现”,这句话有些不太严谨,因为后来有人发现<=>的默认实现在自定义类型带vector成员变量时,性能会有些问题。所以新的C++20标准规定:
<=>的默认实现不再自动生成operator==和operator!=这2种关系运算符。
有了<=>后,编译器甚至不再推荐std::rel_ops的使用了,直接会给出警告(对老兵来说真是残忍啊)。如果非要用这个技巧,那就必须定义SILENCE_CXX20_REL_OPS_DEPRECATION_WARNING
捣蛋鬼
老兵们喜欢摆弄自己心爱的手动武器,新兵们则喜欢随便抓过来一只最新的自动武器就冲向靶场。
“哎,这些新兵蛋子真是越来越堕落了,未来打仗估计都得给他们每个人配一个辅助机器人。”
老兵们太爽<=>的默认实现背着自己搞出来一堆事情,这方便是方便了,却总会让人惴惴不安。很快,一个老兵中的捣蛋鬼就搞出了下面的代码:
struct NumEx { int a; long b; NumEx(int a_ = 0, long b_ = 0) :a(a_), b(b_) { } bool operator<(const NumEx& other) const { return a + b < other.a + other.b; } bool operator==(const NumEx& other) const { return a + b == other.a + other.b; } std::strong_ordering operator <=> (const NumEx&) const = default; }; TEST_METHOD(TestNumEx) { NumEx a(1, 3); NumEx b(2, 1); Assert::IsTrue(a == a); Assert::IsTrue(a != b); Assert::IsTrue(a <=> b == std::strong_ordering::less); Assert::IsTrue(a <=> a == std::strong_ordering::equal); Assert::IsTrue(b <=> a == std::strong_ordering::greater); Assert::IsTrue((a <=> b) < 0); Assert::IsTrue((a <=> a) == 0); Assert::IsTrue((b <=> a) > 0); Assert::IsFalse(a < b); // 用户自定义 Assert::IsTrue(b > a); Assert::IsTrue(a <= b); Assert::IsTrue(b >= a); Assert::IsTrue(a == NumEx(2, 2)); // 用户自定义 Assert::IsTrue(a != b); }
<=>的默认实现再厉害,你还能不让我自定义关系运算符了?于是这里就产生了冲突,到底是用<=>自动生成的关系运算符实现还是用自定义关系运算符的实现呢?
答案当然是后者,无论何时何地,在C++语言中,程序员自定义的优先级最高。
这里,我们故意设计了一个非常另类的比较规则,以区别<=>的默认实现,我们发现:
1、operator<采用了程序员自定义的实现,而operator>,operator<=,operator>=这3个却采用了<=>的默认实现
2、operator==和operator!=采用了程序员自定义的实现,<=>的默认实现果然不再自动生成这2个关系运算符
道理很简单,但是这场面实在是尴尬,新兵们稍有不察都得掉坑里。看来还是得约定一个编码规则:
不允许自定义<=>和自定义关系运算符混用


浙公网安备 33010602011771号