C ++ 20中的比较运算

在科隆举行的会议已经过去,C ++ 20标准已简化为或多或少完成的外观(至少直到特别说明出现之前),我想谈一谈即将到来的创新之一。 这是一种通常称为运算符<=>的机制(标准将其定义为“三向比较运算符”,但非正式的绰号为“太空飞船”),但我认为它的范围要广泛得多。

我们将不仅仅是一个新的运算符-比较的语义将在语言本身的水平上发生重大变化。

即使您无法从本文中得到其他帮助,也请记住以下表格:
平等
精简
基本的
==
<=>
衍生品
!=
<><=> =

现在,我们将有一个新的运算符<=> ,但更重要的是,这些运算符现已系统化。 有基本运算符,也有派生运算符-每个组都有自己的功能。

我们将在简介中简要讨论这些功能,并在以下各节中进行更详细的考虑。

基本运算符可以反转 (即以参数的相反顺序重写)。 可以通过相应的基本语句重写派生语句。 转换后的候选者和重写后的候选者都不会产生新功能;它们只是源代码级别的替代品,而是从扩展的候选者集中选择的。 例如,表达式a <9现在可被评估为a.operator <=>(9)<0 ,表达式10!= B!Operator ==(b,10) 。 这意味着可以省去一个或两个运算符,以实现相同的行为,现在需要手动编写2、4、6甚至12个运算符。 下面将简要介绍这些规则,并列出所有可能的转换。

基本和派生运算符都可以定义为默认值 。 对于基本运算符,这意味着运算符将按照声明顺序应用于每个成员; 对于派生运算符,将使用重写的候选。

应该注意的是,不存在这样一种变换,其中可以用另一种运算符来表示一种运算符(即相等或排序)。 换句话说,我们表中的列绝不相互依赖。 表达式a == b永远不会被隐式地视为运算符<=> (a,b)== 0 (但是,当然,如果需要,您不会使用运算符<=>来定义运算符== )。

考虑一个小的示例,在该示例中我们展示了在应用新功能之前和之后代码的外观。 我们将编写一个不区分大小写的字符串类型CIString ,其对象可以相互比较,也可以与char const *比较

在C ++ 17中,对于我们的任务,我们需要编写18个比较函数:

class CIString { string s; public: friend bool operator==(const CIString& a, const CIString& b) { return assize() == bssize() && ci_compare(asc_str(), bsc_str()) == 0; } friend bool operator< (const CIString& a, const CIString& b) { return ci_compare(asc_str(), bsc_str()) < 0; } friend bool operator!=(const CIString& a, const CIString& b) { return !(a == b); } friend bool operator> (const CIString& a, const CIString& b) { return b < a; } friend bool operator>=(const CIString& a, const CIString& b) { return !(a < b); } friend bool operator<=(const CIString& a, const CIString& b) { return !(b < a); } friend bool operator==(const CIString& a, const char* b) { return ci_compare(asc_str(), b) == 0; } friend bool operator< (const CIString& a, const char* b) { return ci_compare(asc_str(), b) < 0; } friend bool operator!=(const CIString& a, const char* b) { return !(a == b); } friend bool operator> (const CIString& a, const char* b) { return b < a; } friend bool operator>=(const CIString& a, const char* b) { return !(a < b); } friend bool operator<=(const CIString& a, const char* b) { return !(b < a); } friend bool operator==(const char* a, const CIString& b) { return ci_compare(a, bsc_str()) == 0; } friend bool operator< (const char* a, const CIString& b) { return ci_compare(a, bsc_str()) < 0; } friend bool operator!=(const char* a, const CIString& b) { return !(a == b); } friend bool operator> (const char* a, const CIString& b) { return b < a; } friend bool operator>=(const char* a, const CIString& b) { return !(a < b); } friend bool operator<=(const char* a, const CIString& b) { return !(b < a); } }; 

在C ++ 20中,您只能执行4个功能:

 class CIString { string s; public: bool operator==(const CIString& b) const { return s.size() == bssize() && ci_compare(s.c_str(), bsc_str()) == 0; } std::weak_ordering operator<=>(const CIString& b) const { return ci_compare(s.c_str(), bsc_str()) <=> 0; } bool operator==(char const* b) const { return ci_compare(s.c_str(), b) == 0; } std::weak_ordering operator<=>(const char* b) const { return ci_compare(s.c_str(), b) <=> 0; } }; 

我将更详细地告诉您这一切的含义,但首先,让我们回头回顾一下如何比较达到C ++ 20标准。

从C ++ 98到C ++ 17的标准比较


自从创建该语言以来,比较操作没有太大变化。 我们有六个运算符: == !! =<><=> = 。 该标准为内置类型定义了每个类型,但是通常它们遵循相同的规则。 在评估任何a @ b表达式(其中@是六个比较运算符之一)时,编译器将搜索成员函数,自由函数和名为operator @的内置候选对象,可以按指定顺序用类型AB对其进行调用。 从他们中选择最合适的候选人。 仅此而已。 实际上, 所有运算符的工作方式相同:运算<<并没有不同。

这样简单的规则集很容易学习。 所有操作员都是绝对独立和等效的。 我们人类对==!=运算之间的基本关系了解什么都没有关系。 就语言而言,这是相同的。 我们使用成语。 例如,我们定义运算符!=通过==

 bool operator==(A const&, A const&); bool operator!=(A const& lhs, A const& rhs) { return !(lhs == rhs); } 

同样,通过运算符<我们定义所有其他关系运算符。 我们使用这些习惯用法是因为,尽管使用了语言规则,但我们实际上并不认为所有六个运算符都是等效的。 我们接受其中两个是基本的( ==< ),并且已经通过它们表达了所有其他形式。

实际上,标准模板库完全建立在这两个运算符的基础上,被利用代码中的大量类型仅包含其中一个或两个定义。

但是,出于两个原因, <运算符不太适合担任基本角色。

首先,不能保证其他关系运算符通过它来表达。 是的, a> b的含义与b <a完全相同,但a <= b的含义与!(B <a)完全相同并非正确。 如果具有三分法的属性,则后两个表达式将是等效的,其中对于任何两个值,只有以下三个语句之一为true: a <ba == ba> b 。 在存在三分法的情况下,表达式a <= b表示我们正在处理第一种情况或第二种情况……这等效于我们不处理第三种情况的陈述。 因此(a <= b)==!(A> b)==!(B <a)

但是,如果态度不具备三分法的性质怎么办? 这是偏序关系的特征。 一个典型的例子是浮点数,对此,任何操作1.f <NaN1.f == NaN1.f> NaN都会得出false 。 因此, 1.f <= NaN也会说谎 ,但同时!(NaN <1.f)正确的

通过基本运算符一般实现<=运算符的唯一方法是将两个运算都绘制为(a == b)||。 (a <b) ,如果我们仍然必须处理线性顺序,则这是一大步,因为那样的话,将不调用一个函数,而是调用两个函数(例如,表达式“ abc..xyz9” <=“ abc ..xyz1“必须重写为(” abc..xyz9“ ==” abc..xyz1“)||(” abc..xyz9“ <” abc..xyz1“)两次以比较整行)。

其次,由于其在词典词典比较中使用的特殊性,因此它不太适合基本角色。 程序员经常犯此错误:

 struct A { T t; U u; bool operator==(A const& rhs) const { return t == rhs.t && u == rhs.u; } bool operator< (A const& rhs) const { return t < rhs.t && u < rhs.u; } }; 

要为元素集合定义==运算符,只需对每个成员应用一次==就足够了,但这不适用于<运算符。 从该实现的角度来看,集合A {1,2}A {2,1}被认为是等效的(因为它们中的一个都不小于另一个)。 要解决此问题,对除最后一个成员外的每个成员两次应用<运算符:

 bool operator< (A const& rhs) const { if (t < rhs.t) return true; if (rhs.t < t) return false; return u < rhs.u; } 

最后,为了保证异类对象比较的正确操作-即 为了确保表达式a == 1010 ==表示同一意思-他们通常建议将比较写为自由函数。 实际上,这通常是实现此类比较的唯一方法。 这很不方便,因为首先,您必须监视对此建议的遵守情况,其次,通常必须将此类函数声明为隐藏的好友,以实现更方便的实现(即,在类体内)。

注意,比较不同类型的对象时,不一定总是需要写operator ==(X,int) ; 它们也可能意味着int可以隐式转换为X的情况

让我们总结一下C ++ 20标准的规则:

  • 所有语句的处理方式相同。
  • 我们使用成语来促进实现。 运算符==<我们采用基本习语并通过它们表达其余的关系运算符。
  • 仅仅是操作员<不是非常适合该基地的角色。
  • 重要的是(并建议)将异构对象的比较写为自由函数。

新的基本订购运营商:<=>


C ++ 20中比较工作中最重要的变化是添加了一个新的运算符- 运算符<=> ,这是一种三向比较运算符。

我们已经熟悉C语言中的memcmp / strcmp函数和C ++语言中的basic_string :: compare()的三向比较 。 它们都返回int类型的值,如果第一个参数大于第二个参数,则由任意正数表示;如果它们相等,则由0表示;否则,由任意负数表示。

“ spaceship”运算符不返回int值,而是返回属于比较类别之一的对象,该对象的值反映了被比较对象之间的关系类型。 主要分为三类:

  • strong_ordering :线性顺序关系,其中相等意味着元素的互换性(即(a <=> b)== strong_ordering :: equal意味着f(a)== f(b)对所有合适的函数均成立术语“适当的函数”没有明确的定义,但其中不包括返回其参数地址或向量的容量()等的函数。我们仅对“基本”属性感兴趣,这也很模糊,但有条件地可能假设我们在谈论类型的 .vector的值包含在其中 m个元素,但不包括他的地址等)。 此类别包括以下值: strong_ordering ::更大strong_ordering ::等于strong_ordering ::更少
  • weak_ordering :线性顺序关系,其中等式仅定义某个等价类。 一个经典的例子是不区分大小写的字符串比较,当两个对象可以是weak_ordering :: equal ,但不能严格相等时(这解释了用值名称中的equal替换单词equal )。
  • partial_ordering :偏序关系。 在此类别中,一个更大的值,一个相等的值和一个更少的值(如weak_ordering中 )- 无序 (“无序”)被添加一个值。 它可以用于表达类型系统中的部分顺序关系: 1.f <=> NaN给出值partial_ordering :: unordered

您将主要使用strong_ordering类别; 这也是默认情况下使用的最佳类别。 例如, 2 <=> 4返回strong_ordering :: less ,而3 <=> -1 返回strong_ordering :: Greater

可以将较高阶的类别隐式地减少为较弱阶的类别(即, strong_ordering可简化为weak_ordering )。 在这种情况下,将保留当前的关系类型(即, strong_ordering :: equal变为weak_ordering :: equal )。

可以使用六个比较运算符之一将比较类别的值与文字0 (不与任何int且不与int等于0 ,而仅与文字0进行比较):

 strong_ordering::less < 0 // true strong_ordering::less == 0 // false strong_ordering::less != 0 // true strong_ordering::greater >= 0 // true partial_ordering::less < 0 // true partial_ordering::greater > 0 // true // unordered -  ,   //       partial_ordering::unordered < 0 // false partial_ordering::unordered == 0 // false partial_ordering::unordered > 0 // false 

得益于与文字0的比较我们可以实现关系运算符: a @ b等同于每个运算符的(a <=> b)@ 0

例如,可以将2 <4计算为(2 <=> 4)<0 ,这将变成strong_ordering :: less <0,并给出值true

<=>运算符比<运算符更好地适合基本元素的角色,因为它消除了后者的两个问题。

首先,即使部分排序,表达式a <= b也保证等于(a <=> b)<= 0 。 对于两个无序值, a <=> b将给出值partial_ordered :: unordered ,而partial_ordered :: unordered <= 0将给出false ,这是我们需要的。 这是可能的,因为<=>可以返回更多种类的值:例如, partial_ordering类别包含四个可能的值。 类型为bool的值只能为truefalse ,因此在我们无法区分有序值和无序值的比较之前。

为了清楚起见,请考虑与浮点数无关的部分顺序关系的示例。 假设我们要向一个int类型添加一个NaN状态,其中NaN只是一个不与任何涉及的值形成有序对的值。 您可以使用std ::可选来存储它:

 struct IntNan { std::optional<int> val = std::nullopt; bool operator==(IntNan const& rhs) const { if (!val || !rhs.val) { return false; } return *val == *rhs.val; } partial_ordering operator<=>(IntNan const& rhs) const { if (!val || !rhs.val) { //  unordered   //     return partial_ordering::unordered; } // <=>   strong_ordering  int, //        partial_ordering return *val <=> *rhs.val; } }; IntNan{2} <=> IntNan{4}; // partial_ordering::less IntNan{2} <=> IntNan{}; // partial_ordering::unordered //     .    IntNan{2} < IntNan{4}; // true IntNan{2} < IntNan{}; // false IntNan{2} == IntNan{}; // false IntNan{2} <= IntNan{}; // false 

<=运算符返回正确的值,因为现在我们可以在语言本身的层面上表达更多的信息。

其次,要获取所有必要的信息,只需应用一次<=>就足够了,这有助于实现字典比较:
 struct A { T t; U u; bool operator==(A const& rhs) const { return t == rhs.t && u == rhs.u; } strong_ordering operator<=>(A const& rhs) const { //    //  t.   != 0 (..  t // ),    //   if (auto c = t <=> rhs.t; c != 0) return c; //     //    return u <=> rhs.u; }; 

请参阅P0515 ,这是添加运算符<=>的原始语句,以进行更详细的讨论

新的操作员功能


我们不只是为我们提供一个新的运营商。 最后,如果上面显示的带有结构A声明的示例仅表示, 我们现在不必每次写(x <=> y)<0而不是x <y ,那么没人会喜欢它。

C ++ 20中解决比较的机制与旧方法明显不同,但是此更改与两个基本比较运算符的新概念直接相关: ==<=> 。 如果以前是我们惯用的成语(通过==<记录),但是编译器不知道,现在他将理解这种区别。

再一次,我将提供您在本文开头已经看到的表格:
平等
精简
基本的
==
<=>
衍生品
!=
<><=> =

每个基本运算符和派生运算符都有一个新功能,我将再说几句话。

基本运算符的反转


例如,采用只能与int比较的类型:

 struct A { int i; explicit A(int i) : i(i) { } bool operator==(int j) const { return i == j; } }; 

从旧规则的角度来看,表达式a == 10起作用并计算为a.operator ==(10)也就不足为奇了。

但是10 == a呢? 在C ++ 17中,此表达式将被视为明显的语法错误。 没有这样的运算符。 为了使此类代码正常工作,您必须编写一个对称运算符== ,该运算符首先要获取int的值,然后是A ...,并且要实现该功能必须采用自由函数的形式。

在C ++ 20中,基本运算符可以反转。 对于10 == a,编译器将找到候选运算符==(A,int) (实际上,这是一个成员函数,但是为了清楚起见,我在这里将其写为自由函数),然后另外-一个参数相反顺序的变体,即。 运算符==(int,A) 。 第二个候选人与我们的表达方式吻合(理想情况下),因此我们将选择它。 C ++ 20中的表达式10 == a被评估为a.operator ==(10) 。 编译器理解相等是对称的。

现在,我们将扩展类型,以便不仅可以通过相等运算符,而且可以通过排序运算符将其与int进行比较:

 struct A { int i; explicit A(int i) : i(i) { } bool operator==(int j) const { return i == j; } strong_ordering operator<=>(int j) const { return i <=> j; } }; 

同样,表达式a <=> 42可以正常工作,并且根据旧规则作为a.operator <=>(42)计算得出,但是从C ++ 17的角度来看, 42 <=> a是错误的,即使operator < =>已存在于该语言中。 但是在C ++ 20中, 运算符<=>运算符==一样 ,是对称的:它可以识别倒置的候选对象。 对于42 <=> a,将找到成员函数运算符 <=> (A,int) (同样,为更加清楚起见,我在此处将其写为自由函数),以及综合候选运算符 <=> (int,A) 。 反向版本完全符合我们的表达-我们选择它。

但是,不会将42 <=> a计算为a。 运算符 <=> (42) 。 那是错误的。 此表达式的计算结果为0 <=> a.operator <=>(42) 。 尝试弄清楚为什么该输入正确。

重要的是要注意,编译器不会创建任何新函数。 计算10 == a时 ,未出现新的运算符==(int,A) ,而计算42 <=> a时 ,未出现运算符 <=> (int,A) 。 通过反向候选,只有两个表达式被重写。 我重复一遍:没有创建新功能。

还要注意,具有相反参数顺序的记录仅适用于基本运算符,而不适用于派生运算符。 那就是:

 struct B { bool operator!=(int) const; }; b != 42; // ok   C++17,   C++20 42 != b; //    C++17,   C++20 

重写派生运算符


让我们回到结构A的示例:

 struct A { int i; explicit A(int i) : i(i) { } bool operator==(int j) const { return i == j; } strong_ordering operator<=>(int j) const { return i <=> j; } }; 

取表达式a!= 17 。 在C ++ 17中,这是语法错误,因为运算符!=运算符不存在。 但是,在C ++ 20中,对于包含派生比较运算符的表达式,编译器还将搜索相应的基本运算符,并通过它们来表达派生比较。

我们知道在数学中,运算!=本质上表示NOT == 。 现在,编译器已经知道了这一点。 对于表达式a!= 17,他不仅会寻找operator!= Operators ,还会寻找operator == (以及前面的示例中的反向运算符== )。 在此示例中,我们找到了一个最适合我们的相等运算符-我们只需要根据所需的语义重写它: a!= 17将被计算为!(A == 17)

类似地,将17!= A计算为!A.operator ==(17) ,它既是重写版本又是反向版本。

还对订购运营商进行了类似的转换。 如果我们写<9 ,我们将尝试(失败)找到运算符< ,并考虑基本的候选者: 运算符<=> 。 关系运算符的相应替换如下所示: a @ b (其中@是关系运算符之一)的计算公式为(a <=> b)@ 0 。 在我们的例子中, a.operator <=>(9)<0 。 类似地,将9 <= a计算为0 <= a。运算符<=>(9)

请注意,与调用的情况一样,编译器不会为重写的候选项创建任何新功能。 它们的计算方式不同,所有转换仅在源代码级别执行。

以上使我得出以下建议:

仅基本运算 :仅在您的类型中定义基本运算符(==和<=>)。

由于基本运算符会提供整套比较,因此仅定义它们就足够了。 这意味着您只需要2个运算符就可以比较相同类型的对象(而不是现在的6个),并且只需要2个运算符就可以比较不同类型的对象(而不是12个)。 如果只需要相等运算,则只需编写1个函数来比较相同类型的对象(而不是2个)和1个函数来比较不同类型的对象(而不是4个)。 std :: sub_match类是一个极端的情况:在C ++ 17中,它使用42个比较运算符,在C ++ 20中,它仅使用8个比较运算符,而功能不会受到任何影响。

由于编译器还考虑了反向候选,因此所有这些运算符都可以实现为成员函数。 您不再需要为了比较不同类型的对象而编写自由函数。

寻找候选人的特别规则


正如我已经提到的,在C ++ 17中搜索a b的候选对象根据以下原则进行的:我们找到所有运算符@运算符,然后从中选择最合适的一个。

C ++ 20使用扩展的候选集。 现在,我们将搜索所有运算符@ 。 令@@@的基本运算符(可以是同一运算符)。 我们还找到所有运算符@@,并为每个运算符添加其反向版本。 从所有找到的这些候选人中,我们选择最合适的候选人。

请注意, 次操作允许操作员重载。 我们不试图替代其他候选人。 首先,我们将它们全部收集起来,然后从它们中选择最佳的。 如果不存在,则搜索将失败。

现在,我们有更多的潜在候选人,因此还有更多的不确定性。 考虑以下示例:

 struct C { bool operator==(C const&) const; bool operator!=(C const&) const; }; bool check(C x, C y) { return x != y; } 

在C ++ 17中,对于x!= Y ,我们只有一个候选者,现在有三个: x.operator!=(Y) ,!X.operator ==(y)!Y.operator ==(x) 。 选择什么? 他们都是一样的! (注意:候选y.operator!=(X)不存在,因为只能对基本运算符求反 。)

引入了两个附加规则来消除这种不确定性。 未转换的候选人比转换的候选人更可取; . , x.operator!=(y) «» !x.operator==(y) , «» !y.operator==(x) . , «» .

: operator@@ . . , .

-. — (, x < y , — (x <=> y) < 0 ), (, x <=> y void - , DSL), . . , bool ( : operator== bool , ?)

例如:

 struct Base { friend bool operator<(const Base&, const Base&); // #1 friend bool operator==(const Base&, const Base&); }; struct Derived : Base { friend void operator<=>(const Derived&, const Derived&); // #2 }; bool f(Derived d1, Derived d2) { return d1 < d2; } 

d1 < d2 : #1 #2 . — #2 , , , . , d1 < d2 (d1 <=> d2) < 0 . , void 0 — , . , - , #1 .


, , C++17, . , - . :

  • ( )
  • ,
  • , .

, . .

. , , , , , ( ). , :

1
2
a == b
b == a

a != b
!(a == b)
!(b == a)
a <=> b
0 <=> (b <=> a)

a < b
(a <=> b) < 0
(b <=> a) > 0
a <= b
(a <=> b) <= 0
(b <=> a) >= 0
a > b
(a <=> b) > 0
(b <=> a) < 0
a >= b
(a <=> b) >= 0
(b <=> a) <= 0

« » , , .. a < b 0 < (b <=> a) , , , .


C++17 . . :

 struct A { T t; U u; V v; bool operator==(A const& rhs) const { return t == rhs.t && u == rhs.u && v == rhs.v; } bool operator!=(A const& rhs) const { return !(*this == rhs); } bool operator< (A const& rhs) const { //    ,     , //     ?:  &&/|| if (t < rhs.t) return true; if (rhs.t < t) return false; if (u < rhs.u) return true; if (rhs.u < u) return false; return v < rhs.v; } bool operator> (A const& rhs) const { return rhs < *this; } bool operator<=(A const& rhs) const { return !(rhs < *this); } bool operator>=(A const& rhs) const { return !(*this < rhs); } }; 

- std::tie() , .

, : :

 struct A { T t; U u; V v; bool operator==(A const& rhs) const { return t == rhs.t && u == rhs.u && v == rhs.v; } strong_ordering operator<=>(A const& rhs) const { //   T if (auto c = t <=> rhs.t; c != 0) return c; // ...  U if (auto c = u <=> rhs.u; c != 0) return c; // ...  V return v <=> rhs.v; } }; 

. <=> < . , . c != 0 , , ( ), .

. C++20 , :

 struct A { T t; U u; V v; bool operator==(A const& rhs) const = default; strong_ordering operator<=>(A const& rhs) const = default; }; 

, . , :

 struct A { T t; U u; V v; bool operator==(A const& rhs) const = default; auto operator<=>(A const& rhs) const = default; }; 

. , , :

 struct A { T t; U u; V v; auto operator<=>(A const& rhs) const = default; }; 

, , . : operator== , operator<=> .


C++20: . . , , , .


PVS-Studio , <=> . , -. , , (. " "). ++ .

PVS-Studio <, :

 bool operator< (A const& rhs) const { return t < rhs.t && u < rhs.u; } 

. , - . .

: Comparisons in C++20 .

Source: https://habr.com/ru/post/zh-CN465575/


All Articles