【问题标题】:Can a C compiler change bit representation when casting signed to unsigned?将有符号转换为无符号时,C 编译器可以更改位表示吗?
【发布时间】:2023-03-03 02:15:01
【问题描述】:

是否可以通过显式转换(例如,int32_tuint32_t)来更改值的位表示?

例如,假设我有以下联合:

typedef union {
    int32_t signed_val;
    uint32_t unsigned_val;
} signed_unsigned_t;

规范是否保证这些代码段具有相同的行为?

uint32_t reinterpret_signed_as_unsigned(int32_t input) {
    return (uint32_t) input;
}

uint32_t reinterpret_signed_as_unsigned(int32_t input) {
    signed_unsigned_t converter;
    converter.signed_val = input;
    return converter.unsigned_val;
}

我在这里考虑 C99。我见过一些类似的问题,但他们似乎都在讨论 C++,而不是 C。

【问题讨论】:

  • 我怀疑在某些假设情况下可能存在差异,例如在执行 1 补码运算的机器上操作。

标签: c c99 language-lawyer


【解决方案1】:

将有符号整数类型转换为相同宽度的无符号整数类型可以更改表示,如果您能找到具有符号幅度或补码符号表示的机器。但是int32_tuint32_t 类型保证是二进制补码表示,所以在这种特殊情况下,表示不能改变。

有符号整数到无符号整数的转换在标准第 6.3.1.3 节中有明确定义。相关算法是第二段:

  1. 当整数类型的值转换为 _Bool 以外的其他整数类型时,如果 该值可以用新类型表示,它是不变的。
  2. 否则,如果新类型是无符号的,则通过重复添加或转换值 比新类型可以表示的最大值减一 直到值在新类型的范围内。
  3. ...

因此,实际上,如果将负数存储在 2 的补码中,则结果必须是逐位复制的结果。允许一致的实现使用符号幅度或补码;在这两种情况下,都必须修改负整数的表示以强制转换为无符号。


在 cmets 中总结一个冗长而有趣的讨论:

  • 在使用int32_tuint32_t 的OP 中的精确示例中,表示必须相等如果程序编译,因为C99 需要int32_t 和@987654327 @ 正好是 32 位长,没有填充,并且需要 int32_t 使用 2 的补码表示。但是,它不要求这些类型存在;一个补码实现不能简单地定义int32_t,并且仍然符合。

  • 我对类型双关语的解释低于水平规则。 @R.. 向我们指出了 2004 年的 Defect Report,这似乎是说类型双关是可以的,或者引发了一个陷阱,这比未定义的行为更接近实现定义的行为。另一方面,该 DR 的建议解决方案似乎不在 C11 文档中,该文档说 (6.2.6.1(5)):

某些对象表示不需要表示对象类型的值。如果对象的存储值具有这样的表示形式并且被不具有字符类型的左值表达式读取,则行为未定义。

在我看来,如果其中一种参与类型具有陷阱表示,则类型双关语是未定义的行为(因此,如果读取类型没有陷阱,则 not 是未定义的行为表示)。另一方面,任何类型都不需要具有陷阱表示,并且只有少数类型被禁止具有:charunion 类型 - 但不是联合类型的成员 - 以及其中任何一个[u]int*K_t 类型已实现。

我之前关于类型双关的陈述如下:


存储-双关语联合具有未定义的行为。但是在不调用 lagartos voladores 的情况下,如果某个值以无符号形式存储,然后以有符号形式访问,则符号幅度或反码机器可能会引发硬件异常。

二进制补码和符号大小都有两种可能的0 表示形式,一种是每个流行的符号位。带负号位的“负零”允许为“陷阱值”;因此,将值作为有符号整数访问(甚至只是复制它)可能会触发陷阱。

尽管 C 编译器有权抑制陷阱,例如通过使用 memcpy 或无符号操作码复制值,但不太可能这样做,因为这会让知道自己的程序正在运行的程序员感到惊讶在具有捕获负零的机器上,并且期望在非法值的情况下触发陷阱。

【讨论】:

  • 这是否意味着在有符号整数表示为 2 的补码的机器上,这些保证行为相同?还是这种边界未定义的行为并最终给我一个鼻恶魔的坏案例?
  • @AlexandreAraujoMoreira:有一个例外:保证 unsigned int 具有与signed int 相同的位数。它可能有更多的位。 (我也不知道任何这样的架构,但理论上是可能的。)在那种情况下,表示方式有点不一样。反正工会肯定是UB。但是很多人都这样做。
  • 即使使用新的intN_tuintN_t 类型,是否也可以有不同的位数?我以为他们保证有 N 位,但我越看越觉得我很天真。
  • @rici:联合不应该再是 UB(有一个通过定义行为解决的 DR),但这些更改似乎没有使其成为已发布的标准。 .
  • @rici: 不,intN_tuintN_t 保证有 完全 N 位没有填充,并且有符号的保证是二进制补码和有完整的范围(即-INTN_MAX-1 是一个可表示的值)。
【解决方案2】:

在您提到的特定情况下,从int32_tuint32_t 的转换,位表示将是相同的。

该标准特别要求 intN_t 是“宽度为 N 的有符号整数类型,没有填充位,并且是二进制补码表示”。此外,对应的有符号和无符号类型在它们的共享范围内必须具有相同的值表示:

有符号整数类型的有效(非陷阱)对象表示 其中符号位为零是一个有效的对象表示 对应的无符号类型,代表相同的值。

有一个非常可能的小漏洞:原则上,一个实现可以,例如,将int32_t 设为int 的typedef,将uint32_t 设为@ 的typedef 987654327@,其中 intandlong 都是 32 位,但字节顺序不同。但这只会发生在故意不正当的实现中。 更正:这对于符合标准的实现来说是不可能的。 int32_tuint32_t 必须表示对应的有符号和无符号类型。

上述内容仅适用于您碰巧选择了int32_tuint32_t 作为示例,并且标准对它们的表示进行了非常具体的限制。 (如果一个实现不能满足这些限制,那么它就不会定义int32_tuint32_t。)

不过,更一般地说,带符号的类型可以具有以下三种表示形式之一:

  • 符号和幅度,其中将符号位设置为 1 会否定一个数字;
  • 二的补码,其中否定相当于按位补码后加 1;和
  • 一个补码,其中否定等同于按位补码。

绝大多数现代系统都使用二进制补码(并且没有填充位)。在此类系统上,具有相同大小的类型的有符号到无符号转换通常不会更改位表示。 (类型转换的语义是根据值定义的,但旨在方便二进制补码系统。)

但是对于使用符号和幅度或补码的系统,有符号到无符号的转换必须保留值,这意味着负值的转换必须改变表示。

【讨论】:

  • 对于使 uint32_t 和 int32_t 分别为“无符号”和“长”的实现,即使位顺序相同,也可能会导致问题,因为“无符号”和“整数”是别名-compatible,“long”和“unsigned long”也是如此,但一些编译器将“int”和“long”视为不兼容,即使它们的大小相同且具有相同的表示。
  • @supercat:不需要那个分析;它只是不合格的。 N1570 7.20.1p1:“当 typedef 名称仅在初始 u 不存在或存在时有所不同时,它们应表示相应的有符号和无符号类型,如 6.2.5 中所述;提供的实现这些相应类型中的一种也应提供另一种。”我会更正我的答案。顺便说一句,与其说“某些编译器将intlong 视为不兼容”,不如说intlong 不兼容更准确。参见标准中“兼容类型”的定义。
  • 知道是什么激发了定义 uint32_t 的实现还必须定义 int32_t 的要求吗?我认为 32 位补码机器定义 uint32_t 是否可以定义 int32_t 会很有帮助,而且我看不出禁止这样的实现这样做有什么好处。您认为该规则有什么有用的用途吗?
  • 不,我不知道。在绝大多数系统上,这并不重要,但我同意在(罕见的)非 2 补码系统上定义 uint32_t 而不是 int32_t 会更有意义。 C99 基本原理没有解决这个问题。
【解决方案3】:

如果值在有符号和无符号类型的范围内,则值和表示都不会因转换而改变。

否则,只有当实现对类型的负值表示是二进制补码时,才允许有符号到无符号的转换保留位表示。对于补码或符号大小,它的转换必须改变表示。另一个方向的转换是实现定义的,所以它可能会也可能不会改变表示。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2011-04-19
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-02-13
    • 2013-09-01
    • 2023-03-12
    • 1970-01-01
    相关资源
    最近更新 更多