当二进制运算符两侧的有符号性不同时,促销规则是如何工作的?

30

考虑以下程序:

// http://ideone.com/4I0dT
#include <limits>
#include <iostream>

int main()
{
    int max = std::numeric_limits<int>::max();
    unsigned int one = 1;
    unsigned int result = max + one;
    std::cout << result;
}

// http://ideone.com/UBuFZ
#include <limits>
#include <iostream>

int main()
{
    unsigned int us = 42;
    int neg = -43;
    int result = us + neg;
    std::cout << result;
}

+运算符如何“知道”返回哪种类型?通常的规则是将所有参数转换为最宽的类型,但在这里,intunsigned int之间没有明显的“赢家”。在第一种情况下,operator+的结果必须选择unsigned int,因为我得到了一个结果为2147483648。而在第二种情况下,它必须选择int,因为我得到了一个结果为-1。然而,在一般情况下,我不知道这是如何可判定的。我看到的是未定义的行为,还是其他什么情况?


9
std::cout << typeid(x + y).name() 可以快速告诉你一个表达式的类型,至少如果你知道你的实现给不同整数类型的名称。不需要从值中尝试弄清楚它。 - Steve Jessop
5
你也可以让编译器像这样输出错误信息:http://ideone.com/m3cBv - GManNickG
1
@SteveJessop @GManNickG 或者你可以通过定义这个函数 template<typename T> void func(T t) { static_assert(std::is_empty<T>::value, "testing"); } 并将表达式放入函数中,从编译器获取类型。 - Trevor Boyd Smith
3个回答

41

这在§5/9中明确说明:

许多期望算术或枚举类型的操作数的二元运算符会引起转换,并以类似的方式产生结果类型。其目的是产生一个公共类型,也是结果的类型。这种模式被称为“通常的算术转换”,其定义如下:

  • 如果任一操作数的类型为long double,则另一个将被转换为long double
  • 否则,如果任一操作数为double,则另一个将被转换为double
  • 否则,如果任一操作数为float,则另一个将被转换为float
  • 否则,对两个操作数执行整数提升。
  • 然后,如果任何一个操作数是unsigned long,则另一个将被转换为unsigned long
  • 否则,如果一个操作数是long int,另一个是unsigned int,并且long int可以表示所有unsigned int的值,则unsigned int将转换为long int;否则,两个操作数都将转换为unsigned long int
  • 否则,如果任一操作数为long,则另一个将被转换为long
  • 否则,如果任一操作数为unsigned,则另一个将被转换为unsigned

[注意:否则,唯一剩下的情况是两个操作数都是int]。

在您两种情况下,operator+的结果是unsigned。因此,第二种情况实际上是:

int result = static_cast<int>(us + static_cast<unsigned>(neg));

因为在这种情况下,us + neg 的值不能用 int 表示,所以 result 的值是由实现定义的 – §4.7/3:

如果目标类型是有符号的,并且该值可以被表示为目标类型(和位字段宽度),则该值不变;否则,该值是由实现定义的。


2
@Billy: std::cout << static_cast<int>(us + static_cast<unsigned>(neg)); 也会输出 -1。你为什么认为它不能呢?(或者可能是在我第一次编辑之前你问的,如果是这样,请忽略我刚才说的话 :-]) - ildjarn
3
@Billy:你对第二个问题的分析是有缺陷的。 usneg都被转换为无符号数,得到总值UINT_MAX。然后你的实现选择将此值转换为int作为-1。如果你想看到表达式us+neg的值,请执行std::cout << (us+neg),不要在打印之前强制将其转换为int - Steve Jessop
5
@Billy:这是实现定义,而不是未定义行为(4.7/3)。 - Steve Jessop
3
你的基本观点是正确的,原则上它可能在其他地方不起作用。一种实现方式可以定义将任何大于INT_MAXunsigned int值转换为int时结果为0。它只是不能崩溃。 - Steve Jessop
2
@Darren:就实际目的而言,没有任何问题。但我认为这是一种有价值的代码卫生练习,了解您是否以及如何依赖于实现定义的行为。这与二进制补码并不相关 - 二进制补码实现允许进行饱和有符号算术甚至饱和转换。因此,(int)(unsigned)(-1)将是INT_MAX。但重点不在于它是否会真正发生,而在于您的代码是否需要被标记为“不完全可移植”,以便警告那些使用奇怪机器的怪人们;-)。 - Steve Jessop
显示剩余5条评论

12
在C语言标准化之前,编译器之间存在差异——有些遵循“值保持”规则,而其他遵循“符号保持”规则。符号保持意味着如果任一操作数为无符号,则结果也为无符号。这很简单,但有时会产生令人惊讶的结果(特别是当负数转换为无符号数时)。
C标准采用了相对复杂的“值保持”规则。在值保持规则下,类型提升可以/取决于实际类型范围,因此在不同的编译器上可能会得到不同的结果。例如,在大多数MS-DOS编译器上,int与short大小相同,long与两者都不同。在许多当前系统上,int与long大小相同,而short与两者都不同。使用值保持规则,这些情况可能导致提升的类型在两个编译器之间不同。
值保持规则的基本思想是,如果更大的有符号类型可以表示小的类型的所有值,则将其提升为更大的有符号类型。例如,16位无符号短整型可以提升为32位有符号整型,因为每个可能的16位无符号短整型值都可以表示为一个有符号整型。只有在必须保留较小类型的值时(例如,如果unsigned short和signed int都是16位,则signed int无法表示unsigned short的所有可能值,因此将unsigned short提升为unsigned int)才会提升类型为无符号类型。
当您按照所述方式分配结果时,结果将被转换为目标类型,因此在大多数典型情况下,这几乎没有太大影响——至少会将位复制到结果中,并由您决定是否将其解释为有符号或无符号。
但是,当您没有按照所述方式分配结果(例如在比较中)时,情况可能变得非常丑陋。例如:
unsigned int a = 5;
signed int b = -5;

if (a > b)
    printf("Of course");
else
    printf("What!");

根据符号保留规则,b 将被提升为无符号类型,并在此过程中变为 UINT_MAX - 4,因此会执行 if 语句中的"What!"分支。使用值保留规则,你可以 产生一些类似这样的奇怪结果,但是1)主要发生在像DOS系统这样的 intshort 大小相同的系统上,2)而且通常更难做到。


2
"C标准采用了更为复杂的“值保留”规则","在符号保留规则下,b将被提升为无符号数,在此过程中变成等于UINT_MAX - 4,因此if语句的"What!"分支将被执行。但在标准C++中,b会被提升为无符号数,并且"What!"分支会被执行。我认为某些地方出现了错误。" - Steve Jessop
1
关于C语言的关系运算符规则是我非常讨厌的一点。我可以理解不要求编译器处理混合符号情况的理念,尤其是在16位计算机上运行编译器的时代,但标准不允许编译器产生算术正确结果似乎有些令人不悦。 - supercat
1
@supercat:你会如何建议编译器生成“算术正确的结果”? - Billy ONeal
1
@Billy ONeal:如果两个操作数都是有符号或无符号的,则将较小类型扩展为较大类型。如果其中一个是有符号的且为负数,则将其视为较小的操作数。否则将操作数视为无符号的。 - supercat
1
@Jerry Coffin:您能提供任何代码示例,这些代码不会被认为是“编写不良”和“不可移植”,并且在对混合符号表达式进行算术正确评估时将被破坏吗?如果程序员知道一个参数总是适合另一个参数的类型,并且想避免生成额外的代码来处理混合比较,则可以将适合于其他类型的参数进行强制转换。如果程序员想要强制执行特定的语义以防止一个参数不适合另一个类型,则应该进行强制转换。 - supercat
显示剩余10条评论

2

它选择你将结果放入的任何类型,或者至少在输出期间cout会尊重该类型。

我不确定,但我认为C ++编译器为两者生成相同的算术代码,只有比较和输出关心符号。


1
我不明白那怎么可能是这样的。我无论如何都看不出operator+的结果会受到存储结果的位置的影响。我之所以以那种方式存储东西是为了解释,但我同样可以选择两个double类型进行存储,结果是一样的。无论如何,我想看到一个标准参考资料... - Billy ONeal
1
@Billy ONeal,实际的二进制结果是相同的,但你解释结果的方式是不同的。我希望我没有错过整个重点。 - Andrew White
所以,实际上这根本不是解释的问题(至少不是由cout解释)。 - Billy ONeal
1
@Billy:有符号溢出是未定义行为,而不是实现定义的(5/5),尽管你的特定示例 INT_MAX + 1 是一个常量表达式。当然,实现是允许定义这种行为的,我使用过的每个实现都定义了它以进行包装,因此很难观察到溢出的行为不良。我听说过一些情况,真正激烈的优化可能会导致溢出问题。除非另有说明,否则优化器可以合法地假设 x + INT_MAX + 1 是非负的,即使实现对某些整数 x 给出了负值。 - Steve Jessop
1
@supercat:啊,常量表达式是不同的,抱歉。我以为你的例子更为一般化,意思是将任何两个值相乘得到 LONG_MAX。确切的表达式 LONG_MAX * LONG_MAX 是非法的(5/5:“如果在表达式求值期间结果在其类型所能表示的范围之外或数学上未定义,则其行为未定义,除非这样的表达式是常量表达式(5.19),此时程序是非法的。”)。我之前说的适用于 i = j = LONG_MAX; i * j; - Steve Jessop
显示剩余13条评论

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