如果我们在C / C ++中添加安全签名/无符号比较,它会破坏语言或现有代码吗?

在阅读了有关签名/未签名比较的问题之后(他们每隔几天我会说出来):

  • 签名/未签名比较和-Wall

我想知道为什么我们没有正确的签名无符号比较,而是这个可怕的混乱? 从这个小程序中获取输出:

#include  #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%的情况下失败,也取决于类型的具体大小,因此它取决于平台。 以下是一些需要考虑的要点:

  • 转换和比较过程并不是“最小惊喜规则”的主要示例

  • 我不相信那里有代码,它依赖于(short)-1 > (unsigned)1的命题而不是恐怖分子写的。

  • 当你在C ++中使用模板代码时,这一切都很糟糕,因为你需要使用type trait magic来编写正确的“<”。


毕竟,比较不同类型的符号和无符号值容易实现:

 signed X  (a<(X)0) || ((Z)a<(Z)b) where Z=X|Y 

预检是很便宜的,如果可以静态certificate> = 0,编译器也可以对其进行优化。

所以这是我的问题:

如果我们在C / C ++中添加安全签名/无符号比较,它会破坏语言或现有代码吗?

(“它会破坏语言”意味着我们需要对语言的不同部分进行大规模更改以适应这种变化)


更新:我在我的旧版Turbo-C ++ 3.0上运行了这个并得到了这个输出:

 char:1 (signed char)-1 < (unsigned char)1 = 0 

为什么(signed char)-1 < (unsigned char) == 0

是的,它会破坏语言/现有代码。 正如您所指出的,该语言仔细指定了有符号和无符号操作数一起使用时的行为。 对于一些重要的习语,比较运算符的这种行为是必不可少的,例如:

 if (x-'0' < 10U) 

更不用说(等式比较):

 size_t l = mbrtowc(&wc, s, n, &state); if (l==-1) ... /* Note that mbrtowc returns (size_t)-1 on failure */ 

另外,为混合签名/无符号比较指定“自然”行为也会导致显着的性能损失,即使在目前正以安全方式使用此类比较的程序中,由于输入的限制,它们已经具有“自然”行为哪个编译器很难确定(或者可能根本无法确定)。 在编写自己的代码来处理这些测试时,我确信你已经看到了性能损失会是什么样子,而且它并不漂亮。

我的答案仅适用于C.

C中没有类型可以容纳所有可能的整数类型的所有可能值。 最接近的C99是intmax_tuintmax_t ,它们的交点仅覆盖它们各自范围的一半。

因此,您不能通过首先将xy转换为公共类型然后执行简单操作来实现诸如x <= y的数学值比较。 这与运营商如何运作的一般原则背道而驰。 它还打破了操作员对应于通常硬件中的单个指令的事物的直觉。

即使您在语言中添加了这种额外的复杂性(以及实现编写者的额外负担),它也不会具有非常好的属性。 例如, x <= y仍然不等于x - y <= 0 。 如果你想要所有这些不错的属性,你必须将任意大小的整数作为语言的一部分。

我确定那里有很多旧的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 – 和boost – 是一种可怕的可怕语法的例子 – 只有它的父母才会喜欢的那种婴儿 – 并且距离10年前简单优雅(但非常有限)的c ++还有很长的路要走。

关键是,当一个人“修复”某些东西时非常简单,比如整数类型的比较,已经破坏了足够的遗留和现有的c ++代码,人们不妨将其称为新语言。

一旦破碎,还有很多其他东西也有资格进行追溯性修复。

当使用不同C语言类型的组合操作数时,语言定义在运行时可以接近维持最小惊喜原则的规则的唯一方法是让编译器在至少某些上下文中禁止隐式类型转换(将“惊喜”转移到“为什么不编译?”并使其不太可能导致意外错误),为每种存储格式定义多种类型(例如,每种整数类型的包装和非包装变体) ), 或两者。

每种存储格式有多种类型,例如有符号和无符号16位整数的包装和非包装版本,可以让编译器区分“我在这里使用16位值,以防它更高效,但它永远不会超过0-65535的范围,我不会在意它发生了什么 )“和”我使用的是一个需要包裹到65535的16位值,它变为负值“。 在后一种情况下,使用32位寄存器作为此类值的编译器必须在每次算术运算后屏蔽它,但在前一种情况下,编译器可以省略它。 关于您的特定愿望,非包装签名长和非包装无符号长度之间的比较的含义将是清楚的,并且编译器生成实现它所必需的多指令序列是适当的。 (因为将负数转换为非包装unsigned long将是未定义行为,让编译器为这些类型上的比较运算符定义行为不会与可能指定的任何其他操作冲突)。

不幸的是,除了让编译器为混合操作数比较生成警告之外,我并没有真正看到使用C语言可以做到的很多事情,因为它不存在如上所述的新类型; 虽然我认为增加这些新类型是一种改进,但我不会屏住呼吸。

如果整数类型之间的比较比较实际的数学值,我希望在整数和浮点之间进行比较时会发生同样的情况。 并且比较任意64位整数和任意双精度浮点数的精确值是非常困难的。 但是编译器可能比我更好。