在 C 中使用有符号整数执行缩小类型转换的最安全、最明确的方法是什么?

What is the safest and most well-defined way to perform narrowing type conversions with signed integers in C?

提问人:user16217248 提问时间:10/8/2023 最后编辑:user16217248 更新时间:10/9/2023 访问量:90

问:

我读过: 当我在 C 中将 long int 分配给 int 时会发生什么?

int64_t x;
int32_t y;

y = (int32_t)x;

根据答案和 C 标准,如果我尝试将 a 赋值给 a,并且值不合适,则结果是实现定义的,或者会发出实现定义的信号。signed longsigned int

为了解决这种定义不明确的行为,我首先考虑使用无符号类型:

y = (int32_t)(uint32_t)(uint64_t)x;

但这也不行,因为与将较大的整数类型转换为较小的整数类型一样,将无符号整数转换为有符号整数将导致实现定义的值或信号(如果值不合适)。

我想以一种不调用实现定义行为的方式执行缩小转换,例如无符号到有符号或将较大的有符号整数转换为较小的整数,并且只会保留位表示形式(只要符号表示符合预期,这应该会给出非常可预测且定义良好的结果)。有什么方法可以用演员表做到这一点吗?我必须这样使用吗?union

union Cast {
    int32_t i32;
    int64_t i64;
};
y = ((union Cast){.i64 = x}).i32;

这甚至有效吗(即保证只包含与较低的 32 位完全相同的位模式)?作为最后的手段,必须使用,这会起作用吗?yxmemcpy()

编辑:事实证明,使用上述方法将不可移植,因为它是否获得上限或下 32 位取决于系统是大端还是小端。但是,如果我使用它:union

union Cast {
    int32_t i;
    uint32_t u;
};
y = ((union Cast){.u = (uint32_t)x}).i;

那会是便携式的吗?

注意:这个问题是出于好奇而提出的,而且很多时候,出于各种原因,我想安全地截断有符号整数的位。

C 转换 可移植性 有符号 类型缩小

评论

0赞 Some programmer dude 10/8/2023
如果要将较大的值分配给无法容纳如此大值的变量,则没有强制转换或转换可以解决该问题。在这样的转换中,您将始终丢失数据。如果您只需要 64 位值的低 32 位,则执行按位掩码并对结果进行显式转换。例如.y = (int32_t) (x & 0xffffffffllu)
2赞 Some programmer dude 10/8/2023
关于你的工会,成员会得到成员的高位还是低位的 32 位?这还没有明确的定义。i32i64
1赞 user16217248 10/8/2023
@Someprogrammerdude 这是出于好奇,而且很多时候,出于各种原因,我想安全地截断有符号整数的位。
1赞 user3386109 10/8/2023
注意:C23 要求对整数类型使用 2 的补码表示形式。您可能希望获取该标准的副本,并查看现在是否定义了缩小转化范围。
1赞 Ian Abbott 10/8/2023
我认为使用联合来对相同宽度的有符号和无符号“精确宽度”整数类型进行类型双关语(例如 并且 ) 是可移植的,只要实现支持所讨论的确切宽度整数类型。(精确宽度整数类型是可选的,但最常见的系统支持精确宽度 8 位、16 位、32 位和 64 位整数类型。它们保证具有 2 的补码表示,并且没有填充位,因此没有陷阱表示。int32_tuint32_t

答:

1赞 Eric Postpischil 10/9/2023 #1

我想执行缩小转换范围...在某种程度上......将简单地保留位表示(只要符号表示符合预期,这应该会给出非常可预测且定义良好的结果)。

嗯,这是在正确的轨道上,但它缺少一些东西:你没有说预期的标志表示。编写规范的一个专家提示是实际指定您想要的内容。

假设你想要 2 的补码,因为它是最流行的表示方案。等效地,给定一些整数 x,您要转换为有符号的 N 位整数格式,您希望结果 y 与 x 模 2N 一致,其中 −2 N−1 ≤ y < 2 N−1

这些类型保证使用 2 的补码,并且没有填充位 (C 2018 7.20.1.1 1)。所以,如果你能把 x 的低位变成 ,你就完成了。(我还要注意的是,给定有符号整数类型及其对应的无符号整数类型,标准要求它们对应的值位表示相同的值,在 C 2018 6.2.6.2 2 中。intN_tintN_t

您不能依赖具有较宽类型的并集,因为 C 标准没有指定存储中字节的顺序,因此您不知道仅从 C 标准中可以识别较宽类型的哪些字节包含其较低的位。我们可以通过将值转换为第一个值来轻松解决这个问题。从无符号类型范围之外的整数到无符号类型的转换完全由 C 标准定义;它包裹模 2N,产生 [0, 2N] 的结果。uintN_t

在这里,我们执行该转换,并在复合文字中使用并集,以 2 的补码重新解释其结果:

(union { uintN_t u; intN_t i; }) {x} .i

虽然问题中没有提到 C++,但我会注意到 C++ 标准没有定义通过联合重新解释。另一种方法是复制字节:

uintN_t u = x;
intN_t i;
memcpy(&i, &u, sizeof i);

这两者都可移植到支持所需固定宽度类型的任何实现。

这也可以使用算术而不是重新解释字节来完成。假设我们正在从一些具有 M 位的更宽类型转换。首先,我们可以强制转换为以获得低位。然后我们可以调整位 N−1 表示的值:。这个答案解释了这个算术。uintN_t((uintN_t) x ^ ((intM_t) 1 << N-1)) - ((intM_t) 1 << N)

我们还可以使用:

uintN_t u = x;
intN_t i = u - (intM_t) u >> N-1 << N;

这个答案解释了这个算术。

评论

0赞 user16217248 10/9/2023
怎么样_Static_assert(-1==~0, "Not two's complement");