在科隆举行的会议已经过去,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 @的内置候选对象,可以按指定顺序用类型
A或
B对其进行调用。 从他们中选择最合适的候选人。 仅此而已。 实际上,
所有运算符的工作方式
都相同:运算
<与
<并没有不同。
这样简单的规则集很容易学习。 所有操作员都是绝对独立和等效的。 我们人类对
==和
!=运算之间的基本关系了解什么都没有关系。 就语言而言,这是相同的。 我们使用成语。 例如,我们定义运算符
!=通过
== :
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 <b ,
a == b或
a> b 。 在存在三分法的情况下,表达式
a <= b表示我们正在处理第一种情况或第二种情况……这等效于我们不处理第三种情况的陈述。 因此
(a <= b)==!(A> b)==!(B <a) 。
但是,如果态度不具备三分法的性质怎么办? 这是偏序关系的特征。 一个典型的例子是浮点数,对此,任何操作
1.f <NaN ,
1.f == NaN和
1.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 == 10和
10 ==表示同一意思-他们通常建议将比较写为自由函数。 实际上,这通常是实现此类比较的唯一方法。 这很不方便,因为首先,您必须监视对此建议的遵守情况,其次,通常必须将此类函数声明为隐藏的好友,以实现更方便的实现(即,在类体内)。
注意,比较不同类型的对象时,不一定总是需要写
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
得益于与文字
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的值只能为
true或
false ,因此在我们无法区分有序值和无序值的比较之前。
为了清楚起见,请考虑与浮点数无关的部分顺序关系的示例。 假设我们要向一个
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) {
<=运算符返回正确的值,因为现在我们可以在语言本身的层面上表达更多的信息。
其次,要获取所有必要的信息,只需应用一次
<=>就足够了,这有助于实现字典比较:
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 {
请参阅
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;
重写派生运算符
让我们回到结构
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&);
d1 < d2 :
#1 #2 . —
#2 , , , . ,
d1 < d2 (d1 <=> d2) < 0 . ,
void 0 — , . , - ,
#1 .
, , C++17, . , - . :
, . .
. , , , , , ( ). , :
« » , , ..
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 {
-
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 {
.
<=> < . , .
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 .