将uint8_t转换为sint8_t

5

如何在可移植的C代码中将“uint8_t”转换为“sint8_t”的最佳方法。

这是我想出的代码....

#include <stdint.h>

sint8_t DESER_SINT8(uint8_t x)
(
  return
     (sint8_t)((x >= (1u << 8u))
               ? -(UINT8_MAX - x)
               : x);
)

有没有更好/更简单的方法来做这件事?也许有一种不使用条件语句的方法?
编辑:谢谢大家。所以,总结一下,我已经学到了...
- `sint8_t` 真正被称为 `int8_t` - `128` 用 `1 << 7` 表示,而不是用 `1 << 8` 表示 - 二进制补码是“减去1后取反”
:)
因此,这是我原始代码的更新版本:
#include <stdint.h>

int8_t DESER_INT8(uint8_t x)
(
  return ((x >= (1 << 7))
          ? -(UINT8_MAX - x + 1)
          : x);
)

自动转换有什么问题吗? - Donotalo
@Donotalo:自动转换在大正数上是未定义的。这就是OP试图避免的。 - AnT stands with Russia
@Stephen Canon:我的问题实际上是想知道为什么OP要避免自动转换。 - Donotalo
@AndreyT:有多大?多少位或什么数据类型? - Donotalo
2
@Donotalo:在这种情况下,如果 x > 127,自动转换具有实现定义的行为。 - Stephen Canon
显示剩余6条评论
7个回答

13

1u << 8u0x100u,大于所有uint8_t值,因此条件从未得到满足。你的“转换”过程实际上只是:

return x;

实际上这是有一定道理的。

您需要更清晰地定义您想要进行转换的内容。C99将从无符号整数类型到有符号整数类型的转换定义如下(§6.3.1.3“有符号和无符号整数”

  

当带整数类型的值被转换为除_Bool之外的其他整数类型时,如果该值可以由新类型表示,则其不变。

     

...

     

否则,新类型为有符号类型,而且该值无法在其中表示;结果是具体实现定义的,或者引发具体实现定义的信号。

因此,0127之间的uint8_t值将被保留,对于大于127的值的行为是未定义的。许多(但并非所有)实现将简单地将无符号值解释为有符号整数的二进制补码表示。也许你真正想知道的是如何在各个平台上保证这种行为?

如果是这样,您可以使用:

return x < 128 ? x : x - 256;
x - 256是一个int类型,保证将x解释为补码8位整数。 随后对int8_t进行隐式转换可保留此值。 以上假设应理解为int8_t,因为不是标准类型。如果不是,则所有赌注都取消了,因为我建议的转换的正确性取决于int8_t具有二进制补码表示形式的保证(§7.18.1.1“精确宽度整数类型”)。
如果是某些奇怪的平台特定类型,则可能使用其他表示形式,例如以一补数表示。这会产生不同的可表示值集合,从而使所述转换对于某些输入实现定义(因此不可移植)。
编辑
Alf认为这很“愚蠢”,在任何生产系统上都不需要这样做。 我不同意,但必须承认这是边角情况的边角情况。 他的论点并非没有道理。
然而,他声称这是“低效的”,因此应避免使用,这是毫无根据的。 合理的优化编译器将在不需要的平台上对其进行优化。 例如,在x86_64上使用GCC:
#include <stdint.h>

int8_t alf(uint8_t x) {
    return x;
}

int8_t steve(uint8_t x) {
    return x < 128 ? x : x - 256;
}

int8_t david(uint8_t x) {
    return (x ^ 0x80) - 0x80;
}

使用 -Os 和 -fomit-frame-pointer 编译得到以下结果:

_alf:
0000000000000000    movsbl  %dil,%eax
0000000000000004    ret
_steve:
0000000000000005    movsbl  %dil,%eax
0000000000000009    ret
_david:
000000000000000a    movsbl  %dil,%eax
000000000000000e    ret

请注意,优化后所有三种实现是相同的。 Clang / LLVM 给出完全相同的结果。 同样,如果我们构建ARM而不是x86:

_alf:
00000000        b240    sxtb    r0, r0
00000002        4770    bx  lr
_steve:
00000004        b240    sxtb    r0, r0
00000006        4770    bx  lr
_david:
00000008        b240    sxtb    r0, r0
0000000a        4770    bx  lr

保护你的实现,使其在没有成本的“常规”情况下免受边缘情况的影响,从来不是“愚蠢”的。

对于认为这样做增加了无谓的复杂性的观点,我要说:哪个更难——写一个注释来解释转换及其存在的原因,还是你的继任者的实习生在10年后尝试调试问题,当一个新的编译器打破了你一直默默依赖的幸运巧合时?维护以下内容真的那么难吗?

// The C99 standard does not guarantee the behavior of conversion
// from uint8_t to int8_t when the value to be converted is larger
// than 127.  This function implements a conversion that is
// guaranteed to wrap as though the unsigned value were simply
// reinterpreted as a twos-complement value.  With most compilers
// on most systems, it will be optimized away entirely.
int8_t safeConvert(uint8_t x) {
    return x < 128 ? x : x - 256;
}

说了那么多,我同意这有点过头了,但我认为我们应该试着直接回答问题。当然,更好的解决方案是C标准明确规定无填充的二进制补码整数类型(即所有intN_t类型)从无符号类型转换为有符号类型时的行为。


1
@Stephen:不是undefined,而是实现定义或引发实现定义的信号。另外,我认为你颠倒了两种情况,不是吗? - Jens Gustedt
127 是一个有效的值,而 -129 不是。因此 x < 128? x : x - 256 - Potatoswatter
1
@Potatoswatter:字面量256的类型是int,因此从uint8_t中减去它会得到一个int结果(§6.3.1.8“Usual arithmetic conversions”),并且不是无操作。 - Stephen Canon
2
@Alf: 假设不存在这样的实现是不相关的。如果出现一个缺乏本地8位算术的平台,并且在其中夹紧恰好比符号扩展更快(在某些矢量体系结构上这种情况并不荒谬),就可以很容易地获得具有这些属性的实现(如果已经存在一个,我不会感到惊讶)。更重要的是:回答问题时不能改变问题。提问的问题是如何以符合标准的方式进行操作。 - Stephen Canon
1
@Alf:在我的假设场景中,这些类型确实具备所需的属性。uint8_t是一种无符号8位类型。int8_t是一种二进制补码8位类型。然而,从uint8_t转换到int8_t会饱和而不是循环。这并没有违反标准的任何部分,也没有什么是如此不可能以至于你可以将其视为“愚蠢”。 - Stephen Canon
显示剩余12条评论

5

uint8_t转换为int8_t实际上是颠倒了两个半范围的顺序。 "高"数字变成了"低"数字。这可以通过使用异或运算来完成。

x ^ 0x80

然而,所有数值仍然为正。这不好。我们需要引入适当的符号并恢复适当的数量级。

return ( x ^ 0x80 ) - 0x80;

就这样!


1
除了强制转换引入实现定义的行为外。这段代码与简单的强制转换一样不可移植。 - Ben Voigt
1
我的答案中发生了“整数提升”! - heckenpenner_rot
@Stephen:是的,我只是从几天前的讨论中记住了这个... 处理不符合标准或边缘情况的C99实现。 - Potatoswatter
1
(请注意,在大多数平台上,我的条件语句与您的位操作之间的比较效率基本上不是问题;两者都可以完全优化掉) - Stephen Canon
@R..:是的,在这里真的没有意义...我更倾向于将'int'提升为'unsigned int'。 - Potatoswatter
显示剩余6条评论

2

我不知道这是否有任何实际价值,但以下是我想到的一种不同的方法:

uint8_t input;
int8_t output;
*(uint8_t *)&output = input;

请注意:
  • int8_t 必须使用二进制补码表示。
  • 相应的有符号和无符号类型在它们的重叠范围内必须具有相同的表示方式,以便可以通过任一类型的指针访问既是有符号类型又是无符号类型的值。
  • 这只剩下一个比特位,必须是二进制补码的符号位。

我唯一能想到这种推理不成立的情况是 CHAR_BIT>8 并且 8 位整数类型是带有陷阱位的扩展整数类型,该陷阱位会标志该值是有符号还是无符号。然而,以下明确使用 char 类型的类似代码永远不可能失败:

unsigned char input;
signed char output;
*(unsigned char *)output = input;

因为char类型不能有填充/陷阱位。

一个潜在的变体可能是:

return ((union { uint8_t u; int8_t s; }){ input }).s;

或者对于char类型:
return ((union { unsigned char u; signed char s; }){ input }).s;

编辑: 正如Steve Jessop在另一个答案中指出的那样,如果存在填充位,则需要使用int8_tuint8_t,因此它们的存在意味着CHAR_BIT==8。 所以我相信这种方法是有效的。话虽如此,我仍然永远不会使用uint8_t,而是始终明确使用unsigned char,以防实现将uint8_t实现为等大小扩展整数类型,因为char类型对于别名规则和类型转换具有特殊优势,使它们更加可取。


1
出于兴趣,既然这个问题也标记了C ++:在C99中明确允许类型转换,但C ++呢? - Steve Jessop
我从未编写过 C++ 代码,对于C++的技术细节知之甚少。如果您回答了与提问者问题相关且有趣的问题,我会为您点赞。 - R.. GitHub STOP HELPING ICE

0
假设类型 sint8_tuint8_t 可以相互赋值,那么这个代码可以工作。
sint8_t DESER_SINT8(uint8_t x) { return x; }

看看他的代码,pmg,他在做一些不同的事情。他不想要一个转换。 - salezica
2
这违反了标准中的规则[conv.int],该规则指出:“如果目标类型是有符号的,并且该值可以在目标类型(和位域宽度)中表示,则该值不变;否则,该值是实现定义的。” - Ben Voigt

0

嗯,我想你试图返回x,如果x可以用sint8表示,或者abs(SINT8_MAX-x)如果不行,对吧?

在这种情况下,这是一个可行的方案(我认为你的有一个小错误):

#define HIGHBIT(X) ((X) & (1 << (sizeof(X) * 8 - 1)))

char utos8(unsigned char ux)
{
    return HIGHBIT(ux) ? -ux : ux;
}

请注意,使用该代码,您可以将HIGHTBIT宏包装在函数中,从而将任何无符号类型转换为有符号类型。
希望这能帮到您。

假设您的目标平台上 CHAR_BIT == 8 - Stephen Canon
1
没有针对一般整数类型的这样的函数,因为无符号和有符号值的数量可能并不相同。这里之所以能够奏效,是因为你可以假设它是二进制补码 没有陷阱表示。 - Jens Gustedt
@Jens所说的是正确的。然而,它适用于所有C99固定宽度整数类型。 - Stephen Canon
1
@Stephen:如果实现中存在标准的uint8_t,那么CHAR_BIT就是8。这是因为uint8_t被定义为具有8位宽度和无填充位。因此,CHAR_BIT必须能够被8整除,并且不能小于7,因此恰好为8。 - Steve Jessop
@Steve:没错,我指的是“任何无符号类型转有符号类型”的评论。抱歉表达不清楚。 - Stephen Canon
@Steve:实际上,C语言要求CHAR_BIT>=8,而不仅仅是>=7。所以可被整除性并非必需;只需满足8<=CHAR_BIT<=8即可推断出CHAR_BIT==8 - R.. GitHub STOP HELPING ICE

0
如果你想避免分支,你总是可以做一些疯狂的事情,比如这样:
int selector= 127 - x; // 0 or positive if x <=127, negative otherwise
int selector>>= 8; // arithmetic rotate to get -1 or 0
int wrapped_value= x - 256;

return (x&~selector)|(wrapped_value&selector); // if selector is 0, use x, otherwise, use the wrapped value.

1
Potatoswatter已经有了一个没有分支的答案,这与这个问题远不相同。 - Ben Voigt

-1

假设您的sint8_t实际上是来自<stdint.h>int8_t,那么它保证是二进制补码形式,并且保证没有填充位。

进一步假设您希望相反(隐式)转换起作用并产生原始值。

那么,给定类型为uint8_t的值v,您所要做的就是...

    int8_t( v )

就是这样。

据我所知,C标准不保证这种转换,只保证相反的转换。然而,没有已知的系统或编译器不能工作(假设您有这些类型可用)。

忘记所有手动位操作。或者,为了测试您是否正确地进行了转换,只需将该值分配给uint8_t并检查在所有情况下是否获得原始值。特别是,您使用的公式产生-((2^n-1)-x)=1+x-2^n,而值保留的正确转换是x-2^n。

干杯 & hth.,

– Alf


3
问题是如何在“可移植的C语言”中实现这一点。我认为这意味着,“以一种被标准保证在所有符合规范的实现上都能正常工作的方式”。这包括假设的、尚未编写的、超级严谨的实现,它们会故意检查任何转换为有符号类型的无符号值的值,并在它超出范围时发出信号(也许会中止程序)。 - Steve Jessop
@Steve:没有这样的实现,也永远不会有这样的实现,这与担心具有1 GiB bool的C++实现一样无关紧要。我们不为此编写解决方法。请记住,根据C99定义,int8_t是二进制补码。并且请记住,要求使用二进制补码形式的原因正是为了支持这种转换。;-) - Cheers and hth. - Alf
1
@Alf: “所有合理的实现”使用该行为这一事实与问题无关(即使它是真的)。标准没有保证。提问者想知道如何以确保行为符合规范的方式来执行此操作。 - Stephen Canon
1
要求使用二进制补码的原因恰好是为了支持这种转换。或许这是真的,但为什么标准不直接在7.18中定义从“uint8_t”到“int8_t”的转换呢?这样可以避免许多麻烦。说起来,既然表示已经被定义,我怀疑“memcpy(&signed_value,&unsigned_value,1)”是有效的。至于是否“我们”尝试编写严格符合标准的程序:有些人会,有些人不会,通常取决于期限的紧迫程度。如果时间紧迫,我甚至不会在这个网站上回答问题;-) - Steve Jessop
@Steve:我相当肯定 memcpy 解决方案是有效的,就像我给出的基于类型转换的解决方案一样。 - R.. GitHub STOP HELPING ICE
显示剩余3条评论

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