当二元运算符两边的符号不同时,提升规则如何工作? [复制]

Posted

技术标签:

【中文标题】当二元运算符两边的符号不同时,提升规则如何工作? [复制]【英文标题】:How do promotion rules work when the signedness on either side of a binary operator differ? [duplicate] 【发布时间】:2011-10-09 20:38:44 【问题描述】:

考虑以下程序:

// 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 之间没有明确的“赢家”。在第一种情况下,unsigned int 必须被选为operator+ 的结果,因为我得到了2147483648 的结果。在第二种情况下,它必须选择int,因为我得到的结果是-1。然而,在一般情况下,我看不出这是如何确定的。这是我看到的未定义行为还是其他?

【问题讨论】:

FWIW,std::cout &lt;&lt; typeid(x + y).name() 可以快速告诉您表达式的类型,至少如果您知道您的实现为各种整数类型赋予了哪些名称。无需尝试从值中找出它。 你也可以让编译器为你吐出这样的错误:ideone.com/m3cBv @SteveJessop @GManNickG 或者您可以通过定义此函数template&lt;typename T&gt; void func(T t) static_assert(std::is_empty&lt;T&gt;::value, "testing"); 并将表达式放入函数中来从编译器获取类型。 【参考方案1】:

第 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:

如果目标类型是有符号的,如果它可以在目标类型(和位域宽度)中表示,则值不变;否则,该值是实现定义的。

【讨论】:

@Billy : std::cout &lt;&lt; static_cast&lt;int&gt;(us + static_cast&lt;unsigned&gt;(neg)); 也会打印出-1。为什么你会期望它不会呢? (或者你可能在我第一次编辑之前问过,在这种情况下,忽略这个:-]) @Billy:你对第二个的分析是有缺陷的。 usneg 都被转换为无符号,总共产生 UINT_MAX。然后,您的实现选择将此值转换为int-1。如果要查看表达式us+neg 的值,则执行std::cout &lt;&lt; (us+neg),在打印之前不要将值强制为int @Billy:它是实现定义的,而不是 UB (4.7/3)。 您基本上是对的,原则上它可能无法在其他地方工作。一个实现可以定义将任何大于INT_MAXunsigned int 值转换为int 导致值0。它不会崩溃。 @Darren:出于实际目的,不,没有。但我认为这是一个值得进行代码卫生的练习,以了解您是否以及如何依赖于实现定义的行为。这实际上与 2 的补码无关 - 允许 2 的补码实现进行饱和有符号算术甚至饱和转换。所以(int)(unsigned)(-1) 将是INT_MAX。但与其说它是否真的会发生,不如说你的代码是否需要被标记为“不完全可移植”,以便警告那些拥有奇特机器的怪人;-)。【参考方案2】:

在 C 标准化之前,编译器之间存在差异——一些遵循“保留值”规则,而另一些遵循“保留符号”规则。符号保留意味着如果任一操作数是无符号的,则结果是无符号的。这很简单,但有时会产生相当令人惊讶的结果(尤其是当负数转换为无符号时)。

C 对更复杂的“保值”规则进行了标准化。在值保留规则下,提升可以/确实取决于类型的实际范围,因此您可以在不同的编译器上得到不同的结果。例如,在大多数 MS-DOS 编译器上,intshort 的大小相同,而long 则与两者都不同。在许多当前系统上,intlong 的大小相同,而short 则与两者都不同。使用保留值规则,这些可能会导致提升的类型在两者之间有所不同。

值保留规则的基本思想是,如果它可以表示较小类型的所有值,它将提升为较大的有符号类型。例如,可以将 16 位 unsigned short 提升为 32 位 signed int,因为 unsigned short 的每个可能值都可以表示为 signed int。当且仅当有必要保留较小类型的值时,类型才会提升为无符号类型(例如,如果 unsigned shortsigned 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 的腿将被占用。使用保值规则,您可以设法产生一些有点像这样的奇怪结果,但是 1) 主要在类似 DOS 的系统上,intshort 大小相同,并且 2) 无论如何,它通常更难做到。

【讨论】:

“C 标准化了更复杂的“值保留”规则”,“在符号保留规则下,b 将被提升为无符号,并且在此过程中变为等于 UINT_MAX - 4,因此“什么!“如果会被采取的腿”。但在标准 C++ 中,b is 提升为无符号,并且“什么!”腿占用。我认为某处有些地方不对劲。 @Steve 是正确的:ideone.com/gwjBA 或者这只是一个令人困惑的例子,因为符号保留规则和 C 的规则都将操作数转换为 unsigned 关系运算符的规则是我对 C 最不满意的地方之一。我可以理解不要求编译器处理混合符号情况的哲学,尤其是在编译器在 16 位计算机上运行的时代,但标准不允许编译器产生算术正确的结果似乎令人讨厌。 @supercat:您如何建议编译器产生“算术正确的结果”?【参考方案3】:

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

我记不太清了,但我认为 C++ 编译器会为两者生成相同的算术代码,只是比较和输出关心符号。

【讨论】:

我不明白这是怎么回事。我看不出operator+ 的结果如何受到您存储结果的位置的影响。为了解释的目的,我以这种方式存储了东西,但我可以很容易地为两者选择double,结果是一样的。无论如何,我希望看到一个标准参考...... @Billy ONeal,实际的二进制结果是相同的,但您对结果的解释方式不同。我希望我没有错过这里的重点。 不完全。那没有定义。如果它作为有符号整数溢出,则结果将是未定义的。如果它作为一个无符号整数溢出,那么可以依赖于被截断的结果。大多数机器使用二进制补码,但这不是强制性的。 @Billy:有符号溢出是 UB,不是实现定义的(5/5),尽管您的特定示例 INT_MAX + 1 是一个常量表达式。当然,允许实现定义行为,并且我曾经使用过的每个实现都将其定义为环绕,因此很难观察到溢出行为不良。我听说过非常激烈的优化会导致溢出问题的故事。除非另有说明,优化器可以合法地假定 x + INT_MAX + 1 是非负数,即使实现为某些整数x 提供了-ve 值。 @supercat:啊,常量表达式不同,抱歉。我把你的例子举得更笼统,这意味着将任何两个等于LONG_MAX的值相乘。确切的表达式LONG_MAX * LONG_MAX 格式错误(5/5:“如果在计算表达式期间,结果未在数学上定义或不在其类型的可表示值范围内,则行为未定义,除非这样的expression 是一个常量表达式 (5.19),在这种情况下程序是非良构的。”)。我之前说的适用于i = j = LONG_MAX; i * j;

以上是关于当二元运算符两边的符号不同时,提升规则如何工作? [复制]的主要内容,如果未能解决你的问题,请参考以下文章

C语言双目运算符两边的运算数类型不一致系统自动转换的规则是啥?比如1.0/2=0.5那为啥不是1.0/2=0呢?

C语言里且和或是啥符号

java运算符之算数运算符

第二天

关联规则

Linux中设置vim自动在运算符号两边加上空格