离散数学的联动?
前言
C++20中,“比较”这一司空见惯的操作迎来了新客:
头文件<compare>
,以及三向比较操作符(three-way
comparison
operator):<=>
。至今不解为何世多谓之“飞船操作符”(spaceship
operator)
回顾:C++传统的比较操作符有六种:
<
、<=
、>
、>=
、==
、!=
。
且C++20前并没有“互偶”运算符的概念,默认下其并不会认为a == b
的结果就一定是!(a != b)
(事实上如此做是符合逻辑的)。
同时C++20前使用内置运算符进行比较时,必须进行两次比较才能确定两个值的大小关系(大于、小于或等于):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55 class Widget
{
public:
Widget(int dt):data{dt}{}
//便于演示,都用友元(考考你:友元和static区别在哪?)
friend bool operator==(const Widget& x,const Widget& y)
{
return x.data == y.data;
}
friend bool operator!=(const Widget& x,const Widget& y)
{
return !(x == y); //虽然效率不是最高,但多数时候不得不这么写代码,尤其是operator==涉及比较对象多而繁琐时
}
friend bool operator<(const Widget& x,const Widget& y)
{
return x.data < y.data; //烦不烦.sage
}
friend bool operator<=(const Widget& x,const Widget& y)
{
return x.data <= y.data; //烦不烦!
}
friend bool operator>(const Widget& x,const Widget& y)
{
return x.data > y.data; //还没完呢a.a
}
friend bool operator>=(const Widget& x,const Widget& y)
{
return x.data >= y.data; //...
}
private:
int data;
};
void compare_obj(const Widget& x,const Widget& y)
{
if(x > y)
{
std::cout << "x > y" << std::endl;
}
else if(x < y)
{
std::cout << "x < y" << std::endl;
}
else
{
std::cout << "x == y" << std::endl;
}
}
新比较系统简要介绍
新操作符
operator<=>
是一个二元操作符,隐式调用的表达式为a <=> b
;其结合优先级、顺序与其它比较运算符一致。
对于所有的可进行“传统”比较的基本类型(fundamental
types),C++20都为之内置了operator<=>
。
调用operator<=>
时均应包含<compare>
头文件(亦可依C++20之modules,import
之)。
新比较对象
<compare>
中定义了std::strong_ordering
、std::weak_ordering
和std::partial_ordering
中的一种,对应离散数学中的强序、弱序和偏序关系。
operator<=>
的期望返回类型应是上述三种比较结果的一种。
偏序、弱序和强序关系来看,比较的严格程度递增。
偏序关系中,任意两个元素间可能无法比较;
弱序关系中,任意两个元素间必定可以比较,但比较结果的“相等”不蕴含可替换性(即,对于比较a==b
成立时,f(a)==f(b)
不一定成立);
强序关系中,任意两个元素间必定可以比较,且比较结果的“相等”蕴含可替换性。
对于基本类型,operator<=>
的返回值只会是std::strong_ordering
或std::partial_ordering
中的一种(显然基本类型的“相等”都蕴含了可替换性)。
当且仅当对浮点类型(floating point
types)进行比较时,返回std::partial_ordering
(浮点集合中的NaN无法和任何浮点数比较);其它比较时均返回std::strong_ordering
。
每个类型的内置static constexpr
成员如下:
- 偏序关系,
greater
、less
、equivalent
和unordered
;
- 弱序关系,
greater
、less
、equivalent
;
- 强序关系,
greater
、less
、equal
。
可以认为,三向比较操作符就是一次完成了大/等/小的比较,将比较结果存储为一个tag型的对象。
1 | int main() |
C++20语言上对operator<=>的支持
语言层面上,C++20让比较运算符又迈出了关键一步。
运算符的自动“填充”
- 如果一个类定义了
operator<=>
且返回的是上述三种ordering中的一种,则编译器将自动为之生成<
、<=
、>
、>=
四种运算符(自动封装并调用operator<=>
实现)。
- 如果一个类定义了
operator==
或operator!=
且返回的是bool
,则编译器将自动为之生成另一个(调用已定义的运算符,结果逻辑取反)。
上述自动生成的运算符均可以被覆写(自行定义即可),编译器优先使用user-defined的运算符。
老规矩,附上实例: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26class Widget
{
public:
friend constexpr auto operator<=>(const Widget& x,const Widget& y)
{
return x.data<=>y.data;
}
friend constexpr bool operator==(const Widget&,const Widget&)
{
return false; //Debug
}
private:
int data;
};
int main()
{
Widget a{2},b{3};
std::cout << (a < b) << std::endl; //true
std::cout << (a <= b) << std::endl; //false
std::cout << (a != b) << std::endl; //true
return 0;
}
将满足条件的同类比较运算符声明为 =default
同时C++20中支持对满足 一定条件
的类的比较函数通过= default
声明,指示编译器自动生成之。
一定条件 指该类的所有数据成员都应该可以被
operator<=>
和同类对象比较(三向/相等)。
- 对于自动生成的三向运算符,类
Ty
应声明为auto operator<=>(const Ty&) const = default;
或friend auto operator<=>(const Ty&,const Ty&) = default;
。
此时auto
的类型由编译器推导,取所有数据成员比较返回comparison object中对应最弱的比较结果;返回值类型若手动指定,则不能强于该最弱的比较类型。
- 对于自动生成的相等运算符,类
Ty
应声明为bool operator==(const Ty&) const = default;
或friend bool operator==(const Ty&,const Ty&) = default;
。
可以为默认生成的运算符添加
constexpr
和noexcept
修饰符,当然前者要求下辖比较运算符都是constexpr
。
“自动”比较内涵:按变量声明顺序逐个比较,若比较结果不为相等(equivalent或equal)或全部比较结束(直至最后一个仍为相等),则返回之。
1 | class stu |
C++20中,对于既往有同类比较运算符的类(如
std::string
支持和同类比较,行为类似C中的strcmp
),均已经添加了operator<=>
。
如此来看,其实三向比较的思想也并不新鲜——strcmp
就是一种三向比较(返回值大于0/0/小于0),最初的动机即为对于比较时间开销可能较大的object,一次完成对大/等/小的比较并存储起来。
C++20的comparison object相比之的进步在于大幅增强了语义约束,使程序设计的语义更为清晰,可读性和维护性更佳,同时统一并专用化了比较接口(现代C++哲学唠嗑时间)
特例
特别地,如果一个类的operator<=>
被声明为= default;
则该类也会自动生成operator==
。
探究比较对象的性质与应用
为了支持和以往的比较类型有类似的interface,这几种关系都支持了和0做比较(必须是字面量的零,否则报编译错误/UB)。
结合if
的clause-initialization,可以极大地提升程序的可维护性,使执行逻辑更加简洁:
1
2
3
4
5
6
7
8void fun(int arg)
{
if(auto cp = arg <=> 335; cp > 0)
{
std::clog << "Overheat!" << std::endl;
}
}
MSVC中利用C++20的新关键字
consteval
与一些其它有趣的既有语法特性实现了对之的约束,赏析以下代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 struct Literal_zero
{
void Literal_zero_is_expected();
consteval Literal_zero(int _val) noexcept
{
if(_val != 0)
{
Literal_zero_is_expected();
}
}
};
constexpr bool operator>(std::strong_ordering odr,Literal_zero) noexcept
{
return odr == std::strong_ordering::greater;
}
其设置了两道哨兵:
- 其一,如果传入的不是一个字面量,则被
consteval
的语义约束(consteval
函数必须在编译期被调用,但显然传入这个参数不满足约束);
- 其二,若传入的字面量不为零,则试图调用
Literal_zero_is_expected()
函数,但该函数既不是constexpr
也不是consteval
,无法在编译期被调用,因而报错。
另一个性质和离散数学有关:由于偏序-弱序-强序对这种二元关系的约束严格程度递增,因而从语义的角度出发,应当允许更强的比较关系向更弱的比较关系转换,并相应地阻止反方向的转换。
C++中同样如此:如,std::strong_ordering
拥有operator std::weak_ordering
和operator std::partial_ordering
。
同时关于和零比较/拓展运算符的返回结果的一些补充:
对于返回结果cp
,
- 当且仅当返回greater(无论类型,下同),
cp > 0
成立;
- 当且仅当返回less,
cp < 0
成立;
- 当且仅当返回equal/equivalent,
cp == 0
成立;
- 如果返回值是
std::partial_ordering::unordered
,则有且仅有cp != 0
成立,其余均不成立。
- 对于其它的返回值,
>=
就是<
的结果取反,以此类推。
结语
比较运算符的进步明显源于人们在编程实践中的需求,而其又反作用于人们的编程实践中,消除了编写语义重复的运算符重载这一场景,亦为简单类的比较提供了语义更为清晰、简洁的方法(= default;
依然还是如此优雅)。