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个关系运算符

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

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

posted @ 2021-10-28 17:35  胖胖熊猫  阅读(446)  评论(0)    收藏  举报