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++保命手册》,快速翻到实现关系运算符的部分:

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

因为浮点数有精度问题,即使相等也不一定可以互换,所以它是弱顺序比较。

还有其它类型的比较,不过都大同小异,就不多说了。

对于新兵来说,真正关键的是,<=>运算符的默认实现是全自动的:

  1. 会替你比较自定义类型中每一个成员变量
  2. 会替你处理基类、子对象等这些细节
  3. 会替你自动完成关系运算符的实现
		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个关系运算符

道理很简单,但是这场面实在是尴尬,新兵们稍有不察都得掉坑里。看来还是得约定一个编码规则:

不允许自定义<=>和自定义关系运算符混用

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值