老兵
作为一名在战场上出生入死多年的老兵,对于手中的武器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++保命手册》,快速翻到实现关系运算符的部分:
#include "pch.h"
#include "CppUnitTest.h"
#include <utility>
using namespace Microsoft::VisualStudio::CppUnitTestFramework;
using namespace std;
using namespace std::rel_ops;
namespace UnitTestCpp20
{
TEST_CLASS(UnitTestThreeWay)
{
public:
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个关系运算符
道理很简单,但是这场面实在是尴尬,新兵们稍有不察都得掉坑里。看来还是得约定一个编码规则:
不允许自定义<=>和自定义关系运算符混用