将无符号整数与负字面量比较

3

我有一个简单的 C 程序。

#include <stdlib.h>
#include <stdio.h>
#include <stdbool.h>

bool foo (unsigned int a) {
    return (a > -2L);
}

bool bar (unsigned long a) {
    return (a > -2L);
}

int main() {
    printf("foo returned = %d\n", foo(99));
    printf("bar returned = %d\n", bar(99));
    return 0;
}

当我运行这个程序时的输出如下:

foo returned = 1
bar returned = 0

这里有在godbolt上重新创建的链接here

我的问题是为什么foo(99)返回true但bar(99)返回false。

对我来说,bar返回false是有道理的。为了简单起见,假设长整型为8位,然后(使用补码表示有符号值):

99 == 0110 0011
-2 == unsigned 254 == 1111 1110

显然,CMP指令将看到1111 1110更大,并返回false。

但是我不明白foo函数背后正在发生什么。 foo的汇编似乎硬编码为始终返回mov eax,0x1。 我本来希望foo做类似于bar的事情,这里到底发生了什么?


2
请选择一个编程语言进行咨询。虽然C和C++看起来很相似,但它们实际上是两种非常不同的语言,在许多情况下具有不同的行为。 - Some programmer dude
请记住,您正在将其与值为-2long类型进行比较,对于foo函数而言,这意味着无符号整数变量a将被转换为普通的long类型。算术运算(包括比较操作)使用更大的数据类型进行计算。 - Some programmer dude
3
在OP的情况下,只有当long可以表示所有unsigned int的值时,unsigned int才会被转换为long。 如果longunsigned int具有相同的宽度,则通常算术转换的最后一条规则适用,即两个操作数都将转换为与有符号整数操作数类型相对应的无符号类型,这将是unsigned long。请注意,不改变原文意思的前提下,本翻译可能不如原文精准。 - Eric Postpischil
1
没有负面的文字字面值。 - Language Lawyer
4个回答

4

这个问题在C类中得到了解决,并在文档中进行了说明。以下是使用文档来解决此问题的方法。

2018年的C标准中,您可以查找索引中的>或“关系表达式”,以查看它们在第68-69页上被讨论。在第68页中,你会发现第6.5.8条款,其中涵盖了关系运算符,包括>。阅读它,在第3段中说:

如果两个操作数具有算术类型,则执行通常的算术转换。

“通常的算术转换”在索引中列出,定义在第39页。第39页有第6.3.1.8条款,“通常算术转换”。这个条款解释了算术类型的操作数被转换为一个公共类型,并给出了确定公共类型的规则。对于不同带符号性的两个整型,例如bar中的unsigned longlong inta-2L),它说,如果无符号类型的级别大于或等于另一类型的级别,则签名类型将转换为无符号类型。

“级别”不在索引中,但您可以搜索文档找到它是在第6.3.1.1条款中讨论的,它告诉您long int的级别大于int的级别,并且任何无符号类型与相应类型具有相同的级别。

现在您可以考虑在bar中使用a > -2L,其中aunsigned long。在这里,我们比较了一个unsigned long和一个long。他们有相同的级别,所以-2L被转换为unsigned long。有符号整数转换为无符号整数在第6.3.1.3条款中讨论。它说,该值通过使用模ULONG_MAX+1进行包装转换,因此将有符号长整型−2转换产生ULONG_MAX+1−2 = ULONG_MAX−1,这是一个大整数。然后将值99的a与使用>的大整数进行比较,结果为false,因此返回零。

对于foo,我们遵循通常算术转换的规则。当无符号类型的等级不大于或等于有符号类型的等级时,但是有符号类型可以表示带有无符号类型操作数类型的所有值时,使用无符号类型操作数转换为有符号类型操作数。在foo中,aunsigned int,而-2Llong int。假设在您的C实现中,long int为64位,因此它可以表示32位unsigned int的所有值。因此,应用此规则,将a转换为long int。这并不改变值。因此,将a的原始值99与-2进行比较,并使用>运算符,得到true,因此返回1。


谢谢。我一直在回避查看标准,但现在我要咬紧牙关了。 - Sourav Ganguly

2

在第一个函数中

bool foo (unsigned int a) {
    return (a > -2L);
}

表达式 a > -2L 的两个操作数都是类型为 long 的(由于通常算术转换的缘故,第一个操作数被转换为类型 long,因为类型 long 的等级高于类型 unsigned int 的等级,并且在使用的系统中,类型 unsigned int 的所有值都可以由类型 long 表示)。很明显,正值 99L 大于负值 -2L

如果 sizeof(long) 等于 sizeof(unsigned int),则第一个函数可能会产生结果 0。在这种情况下,类型 long 无法表示类型 unsigned int 的所有(正)值。因此,由于通常的算术转换,两个操作数都将转换为类型 unsigned long

例如,在使用 MS VS 2019 运行函数 foo,其中 sizeof(long) 等于 4,就会得到结果 0

下面是一段用 C++ 编写的演示程序,直观地展示了为什么使用 MS VS 2019 调用函数 foo 的结果可能等于 0

#include <iostream>
#include <iomanip>
#include <type_traits>

int main()
{
    unsigned int x = 0;
    long y = 0;

    std::cout << "sizeof( unsigned int ) = " << sizeof( unsigned int ) << '\n';
    std::cout << "sizeof( long ) = " << sizeof(long) << '\n';

    std::cout << "std::is_same_v<decltype( x + y ), unsigned long> is "
              << std::boolalpha
              << std::is_same_v<decltype( x + y ), unsigned long>
              << '\n';
}

程序的输出结果是:
sizeof( unsigned int ) = 4
sizeof( long ) = 4
std::is_same_v<decltype( x + y ), unsigned long> is true

通常情况下,第一个函数的结果是实现定义的。

在第二个函数中

bool bar (unsigned long a) {
    return (a > -2L);
}

由于通常的算术转换和类型unsigned long和signed long的等级相等,因此两个操作数都具有类型unsigned long(所以类型为signed long的对象被转换为类型unsigned long),并且将-2L解释为unsigned long大于99。


2
这是由于整数转换规则的原因。
在第一种情况下,您使用“>”运算符将无符号整数与长整型进行比较,在第二种情况下,您将无符号长整数与长整型进行比较。
这些操作数必须首先使用“通常的算术转换”转换为公共类型。这些在C标准的6.3.1.8p1节中详细说明,以下摘录重点关注整数转换:
如果两个操作数具有相同的类型,则不需要进行进一步转换。
否则,如果两个操作数都具有有符号整数类型或都具有无符号整数类型,则具有较小整数转换等级的操作数将被转换为具有更大等级的操作数的类型。
否则,如果具有无符号整数类型的操作数的等级大于或等于另一个操作数的类型的等级,则具有有符号整数类型的操作数将被转换为具有无符号整数类型的操作数的类型。
否则,如果具有有符号整数类型的操作数的类型可以表示无符号整数类型的所有值,则具有无符号整数类型的操作数将被转换为具有有符号整数类型的操作数的类型。
否则,两个操作数都将转换为与具有有符号整数类型的操作数的类型相对应的无符号整数类型。
在比较一个无符号整数(unsigned int)和一个长整型(long)时,第二段加粗的文字适用。由于long的优先级更高,并且(假设long为64位,int为32位),可以保存所有值,因此无符号整数操作数a被转换为long。由于所涉及的值在long范围内,因此根据6.3.1.3p1节规定如何进行转换:

当将具有整数类型的值转换为除 _Bool 以外的另一种整数类型时,如果该值可以用新类型表示,则保持不变。

因此,该值被保留下来,我们得到了99 > -2,这是真的。
在比较无符号长整型和长整型时,第一个加粗的段落适用。这两种类型具有相同的等级但不同的符号,因此长整型常量“-2L”被转换为无符号长整型。-2超出了无符号长整型的范围,因此必须进行值转换。该转换在第6.3.1.3p2节中指定:
“否则,如果新类型是无符号的,则通过反复添加或减去可以在新类型中表示的最大值加上一直到该值在新类型的范围内为止。”
因此,长整型值-2将被转换为无符号长整型值2^64-2,假设无符号长整型为64位。 因此我们得到99>2^64-2,这是错误的。

0

我认为这里发生的是编译器的隐式提升。当您对两个不同的基元执行比较时,编译器将促使其中一个与另一个相同类型。我相信规则是使用具有更大可能值的类型作为标准。 因此,在foo()中,您会将参数隐式提升为有符号长整型,并且比较按预期工作。 在bar()中,您的参数是无符号长整型,其最大值比有符号长整型大。在这里,编译器将-2L提升为无符号长整型,这变成了一个非常大的数字。


2
编译器将-2L提升为无符号长整型。实际上这是一种算术转换,而不是提升到无符号长整型。 - eerorika

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接