在阅读了有关签名/未签名比较的问题之后(我每隔几天就会说出来):
我想知道为什么我们没有正确的签名无符号比较,而是这个可怕的混乱?从这个小程序中获取输出:
#include <stdio.h>
#define C(T1,T2)\
{signed T1 a=-1;\
unsigned T2 b=1;\
printf("(signed %5s)%d < (unsigned %5s)%d = %d\n",#T1,(int)a,#T2,(int)b,(a<b));}\
#define C1(T) printf("%s:%d\n",#T,(int)sizeof(T)); C(T,char);C(T,short);C(T,int);C(T,long);
int main()
{
C1(char); C1(short); C1(int); C1(long);
}
用我的标准编译器(gcc,64bit)编译,我得到这个:
char:1
(signed char)-1 < (unsigned char)1 = 1
(signed char)-1 < (unsigned short)1 = 1
(signed char)-1 < (unsigned int)1 = 0
(signed char)-1 < (unsigned long)1 = 0
short:2
(signed short)-1 < (unsigned char)1 = 1
(signed short)-1 < (unsigned short)1 = 1
(signed short)-1 < (unsigned int)1 = 0
(signed short)-1 < (unsigned long)1 = 0
int:4
(signed int)-1 < (unsigned char)1 = 1
(signed int)-1 < (unsigned short)1 = 1
(signed int)-1 < (unsigned int)1 = 0
(signed int)-1 < (unsigned long)1 = 0
long:8
(signed long)-1 < (unsigned char)1 = 1
(signed long)-1 < (unsigned short)1 = 1
(signed long)-1 < (unsigned int)1 = 1
(signed long)-1 < (unsigned long)1 = 0
如果我编译为32位,结果是相同的,除了:
long:4
(signed long)-1 < (unsigned int)1 = 0
“如何?”所有这些都很容易找到:只需转到C99标准的第6.3节或C的第4章,并挖掘描述操作数如何转换为通用类型的子句,如果常见类型重新解释负值,这可能会中断 .
但是“为什么?”呢?正如我们所看到的,'<'在50%的情况下失败,也取决于类型的具体大小,因此它取决于平台 . 以下是需要考虑的一些要点:
-
转换和比较过程实际上不是最小惊喜规则的主要例子
-
我不相信那里有代码,这些代码依赖于恐怖主义分子所写的命题 .
-
当你在C中使用模板代码时,这很可怕,因为你需要使用type trait magic来编织正确的“<” .
毕竟,比较不同类型的有符号和无符号值很容易实现:
signed X < unsigned Y -> (a<(X)0) || ((Z)a<(Z)b) where Z=X|Y
预检是便宜的,如果可以静态证明> = 0,编译器也可以对其进行优化 .
所以这是我的问题:
Would it break the language or existing code if we'd add safe signed/unsigned compares to C/C++?
(“它会破坏语言”意味着我们需要对语言的不同部分进行大量更改以适应这种变化)
UPDATE: 我在我的老式Turbo-C 3.0上运行了这个并得到了这个输出:
char:1
(signed char)-1 < (unsigned char)1 = 0
为什么 (signed char)-1 < (unsigned char) == 0
在这?
6 回答
是的,它会破坏语言/现有代码 . 正如您所指出的,该语言仔细指定了有符号和无符号操作数一起使用时的行为 . 对于一些重要的习语,比较运算符的这种行为是必不可少的,例如:
更不用说(等式比较):
另外,为混合签名/无符号比较指定“自然”行为也会导致显着的性能损失,即使在目前正以安全方式使用此类比较的程序中,由于输入的限制,它们已经具有“自然”行为哪个编译器很难确定(或者可能根本无法确定) . 在编写自己的代码来处理这些测试时,我确信你已经看到了性能损失会是什么样子,而且它并不漂亮 .
我的答案仅适用于C.
C中没有类型可以容纳所有可能的整数类型的所有可能值 . 最接近的C99是
intmax_t
和uintmax_t
,它们的交点仅覆盖各自范围的一半 .因此,您无法通过首先将
x
和y
转换为公共类型然后执行简单操作来实现诸如x <= y
之类的数学值比较 . 这与运营商如何运作的一般原则背道而驰 . 它还打破了操作员对应于普通硬件中往往是单指令的事物的直觉 .即使您在语言中添加了这种额外的复杂性(以及实现编写者的额外负担),它也不会具有非常好的属性 . 例如,
x <= y
仍然不等于x - y <= 0
. 如果你想要所有这些不错的属性,你必须将任意大小的整数作为语言的一部分 .我'm sure there'有很多旧的unix代码,可能有一些在你的机器上运行,假设
(int)-1 > (unsigned)1
. (好吧,也许是自由战士写的;-)如果你想要lisp / haskell / python / $ favorite_language_with_bignums_built_in,你知道在哪里找到它......
我不认为它会破坏语言,但是,它可能会破坏一些现有的代码(并且在编译器级别可能很难检测到破坏) .
用C和C编写的代码比你和我一起想象的要多得多(其中一些甚至可能是恐怖分子编写的) .
依靠“
(short)-1 > (unsigned)1
”的命题可能会被某人无意中完成 . 存在许多处理复杂位操作和类似事物的C代码 . 一些程序员很可能正在使用当前的程序员这种代码中的比较行为 . (其他人已经提供了很好的代码示例,代码甚至比我预期的更简单) .当前的解决方案是警告这样的比较,并将解决方案留给程序员,我认为这是C和C的工作原理 . 此外,在编译器级别上解决它会导致性能损失,这是C和C程序员非常敏感的事情 . 两个测试而不是一个测试对你来说似乎是一个小问题,但可能有很多C代码,这将是一个问题 . 它可以解决,例如通过使用显式转换强制执行以前的行为到公共数据类型 - 但这又需要程序员注意,因此它并不比简单的警告更好 .
我认为C就像罗马帝国 . 它很大,而且太固定,无法修复破坏它的东西 .
c 0x - 和提升 - 是一种可怕的可怕语法的例子 - 只有它的父母才会喜欢的那种婴儿 - 并且与10年前的简单优雅(但非常有限)相比还有很长的路要走 .
关键是,当一个人“修复”某些东西时非常简单,比如整数类型的比较,已经破坏了足够的遗留和现有的c代码,人们也可以将其称为新语言 .
一旦破损,还有很多其他东西也有资格进行追溯性修复 .
在使用不同C语言类型的组合操作数时,语言在运行时定义可以接近维持最小惊喜原则的规则的唯一方法是让编译器在至少某些上下文中禁止隐式类型转换(将“惊喜”转移到“为什么不编译?”并使其不太可能导致意外错误),为每种存储格式定义多种类型(例如,每种整数类型的包装和非包装变体) ), 或两者 .
每种存储格式具有多种类型,例如有符号和无符号16位整数的包装和非包装版本都可以允许编译器区分“我'm using a 16-bit value here in case it makes things more efficient, but it' s永远不会超出范围0-65535而我不会在意它发生了什么事情”" and " I我使用的是一个需要包装到65535的16位值,它变为负数“ . 在后一种情况下,使用32位寄存器获取此类值的编译器必须在每次算术运算后屏蔽它,但在前一种情况下,编译器可以省略它 . 关于您的特定愿望,非包装签名长和非包装无符号长度之间的比较的含义将是清楚的,并且编译器生成实现它所必需的多指令序列是适当的 . (因为将负数转换为非包装
unsigned long
将是未定义行为,让编译器为这些类型上的比较运算符定义行为不会与可能指定的任何其他操作冲突) .不幸的是,除了让编译器为混合操作数比较生成警告之外,我并没有真正看到可以用C语言做的很多事情,因为它不存在如上所述的新类型;虽然我认为增加这些新类型是一种改进,但我不会屏住呼吸 .
如果整数类型之间的比较比较实际的数学值,我希望在整数和浮点之间进行比较时会发生同样的情况 . 并且比较任意64位整数和任意双精度浮点数的精确值是非常困难的 . 但是编译器可能比我更好 .