避免实现定义行为的高效无符号到有符号转换

111

我想定义一个函数,接受一个无符号整数作为参数,并返回与参数模UINT_MAX+1同余的int。

第一次尝试可能是这样的:

int unsigned_to_signed(unsigned n)
{
    return static_cast<int>(n);
}

但是任何语言专家都知道,对于大于INT_MAX的值从无符号类型转换为有符号类型是由具体实现定义的。

我希望实现这个功能只依赖于规范强制要求的行为,并且它可以在任何现代计算机和优化编译器上编译成无操作的代码。

至于奇怪的机器... 如果没有一个有符号整数与无符号整数模UINT_MAX + 1同余,那么我想抛出异常。如果有多个(我不确定这是否可能),那么我想选择最大的那个。

好的,第二次尝试:

int unsigned_to_signed(unsigned n)
{
    int int_n = static_cast<int>(n);

    if (n == static_cast<unsigned>(int_n))
        return int_n;

    // else do something long and complicated
}

在我不处于典型的二进制补码系统时,我并不太在意效率,因为在我看来这是不可能的。如果我的代码在2050年无处不在的原码系统上成为瓶颈,那么我打赌有人可以找出并进行优化。

现在,这第二次尝试非常接近我的要求。虽然对于某些输入,将其转换为int是实现定义的,但按照标准,将其转换回unsigned将保留模 UINT_MAX+1的值。因此,条件确实检查了我想要的内容,并且它将在我可能遇到的任何系统上编译成无操作。

然而... 我仍然先将其转换为int,而没有先检查它是否会激发实现定义的行为。在2050年的某个假设系统上,它可能会做出谁知道什么样的事情。所以,我想避免这种情况。

问题:我“第三次尝试”应该是什么样子的?

总之,我想要:

  • 从无符号整数转换为有符号整数
  • 保留值mod UINT_MAX+1
  • 仅调用标准规定的行为
  • 在具有优化编译器的典型二进制补码机器上编译成无操作

[更新]

让我举个例子,以说明这不是一个琐碎的问题。

考虑具有以下属性的假想C++实现:

  • sizeof(int)等于4
  • sizeof(unsigned)等于4
  • INT_MAX等于32767
  • INT_MIN等于-232+32768
  • UINT_MAX等于232-1
  • int上的算术运算是模232(进入范围INT_MININT_MAX)
  • std::numeric_limits<int>::is_modulo为true
  • 将无符号n转换为整数保留0<=n<=32767的值,并产生

在这个假设的实现中,每个无符号值都恰好与一个int值同余(模 UINT_MAX+1)。因此,我的问题是明确定义的。

我声称这个假设的C++实现完全符合C++98、C++03和C++11规范。我承认我没有记住它们的每一个单词......但我相信我已经仔细阅读了相关部分。所以如果你想让我接受你的答案,你必须要么(a)引用一项排除了这个假设实现的规范,要么(b)正确处理它。

事实上,正确的答案必须处理标准允许的每一个假设的实现。这就是“只调用标准规定的行为”所定义的含义。

顺便说一句,注意std::numeric_limits<int>::is_modulo在这里是完全无用的,有多种原因。首先,即使对于大的无符号值,它也可能是true,即使无符号到有符号的转换不起作用。其次,即使在一补数或符号-大小系统上,如果算术运算仅模整个整数范围,它也可能是true。如果你的答案依赖于is_modulo,那么它就是错误的。

[更新2]

hvd的答案教会了我一些东西:我的假设整数C++实现被现代C所允许。C99和C11标准非常明确地规定了有符号整数的表示方式;事实上,它们只允许二补数、一补数和符号-大小(第6.2.6.2段;)。

但是C++不是C。事实证明这个事实正是我的问题的核心。

C++98标准最初基于更旧的C89,后者说(第3.1.2.5节):

对于每个有符号整数类型,都有一个相应的(但不同的)无符号整数类型(用关键字unsigned指定),使用相同数量的存储空间(包括符号信息)并具有相同的对齐要求。有符号整数类型的非负值范围是相应无符号整数类型的子范围,同一值在每种类型中的表示方式相同。

C89没有提到只有一个符号位或只允许二补数/一补数/符号-大小。

C++98标准几乎照搬了这种语言(第3.9.1段(3)):

对于每种有符号整数类型,都存在一种相应的(但不同的)无符号整数类型:“unsigned char”,“unsigned short int”,“unsigned int”和“unsigned long int”,每种类型占用与相应的有符号整数类型相同的存储空间,并具有相同的对齐要求(3.9)。也就是说,每个有符号整数类型具有与其对应的无符号整数类型相同的对象表示形式。有符号整数类型的非负值范围是相应无符号整数类型的子范围,并且每个相应的有符号/无符号类型的值表示应该相同。 C++03标准使用基本相同的语言,C++11也是如此。 据我所知,没有任何标准的C ++规范将其有符号整数表示约束为任何C规范。它没有规定单个符号位或任何类似的内容。它只是说,非负有符号整数必须是相应无符号整数的子范围。 因此,我再次声明INT_MAX = 32767,INT_MIN = -2 ^ 32 + 32768是允许的。除非引用证明我错误的C ++标准,否则您的答案假设错误。

@SteveJessop:实际上,在那种情况下,我已经明确说明我想要什么了:“如果没有一个有符号整数与无符号整数模UINT_MAX+1同余,那么我想抛出一个异常。”也就是说,我想要“正确”的有符号整数,只要它存在。如果它不存在——例如填充位或补码表示的情况可能会发生——我希望检测到并处理该特定转换调用的情况。 - Nemo
抱歉,不确定我是怎么错过那个的。 - Steve Jessop
顺便说一下,我认为在你的假设棘手实现中,“int”至少需要33位才能表示它。我知道这只是一个脚注,所以你可以认为它是非规范性的,但我认为C++11中的脚注49旨在是真实的(因为它是标准中使用的术语的定义),并且它不会与规范文本中明确说明的任何内容相矛盾。因此,所有负值必须由最高位设置的位模式表示,因此您无法将其中的2 ^ 32-32768个值压缩到32位中。并不是你的论点以任何方式依赖于“int”的大小。 - Steve Jessop
关于您对hvd答案的编辑,我认为您误解了注释49。您说补码是被禁止的,但实际上并不是。您将其理解为:“由连续位表示的值是可加的,以1开头,并且(乘以连续整数次幂的2,除了最高位可能除外)”。我认为应该这样理解:“由连续位表示的值(是可加的,以1开头,并且乘以连续整数次幂的2),除了最高位可能除外”。也就是说,如果设置了高位,则所有赌注都无效。 - Steve Jessop
@SteveJessop:你的解释可能是正确的。如果是这样,它排除了我的假设......但它也引入了大量的可能性,使得这个问题非常难以回答。对我来说,这实际上看起来像是规范中的一个错误。(显然,C委员会也这么认为,并在C99中彻底修复了它。我想知道为什么C++11没有采用他们的方法?) - Nemo
显示剩余4条评论
8个回答

80

扩展user71404的答案:

int f(unsigned x)
{
    if (x <= INT_MAX)
        return static_cast<int>(x);

    if (x >= INT_MIN)
        return static_cast<int>(x - INT_MIN) + INT_MIN;

    throw x; // Or whatever else you like
}

如果 x >= INT_MIN(请记住提升规则,INT_MIN 被转换为 unsigned),那么 x - INT_MIN <= INT_MAX,因此这不会有任何溢出。

如果这不是显然的,请看一下声明“如果 x >= -4u,那么 x + 4 <= 3。”,并记住 INT_MAX 将至少等于 -INT_MIN - 1 的数学值。

在最常见的系统上,其中 !(x <= INT_MAX) 意味着 x >= INT_MIN,优化器应该能够(并且在我的系统上能够)删除第二个检查,确定两个 return 语句可以编译为相同的代码,并删除第一个检查。生成的汇编列表:

__Z1fj:
LFB6:
    .cfi_startproc
    movl    4(%esp), %eax
    ret
    .cfi_endproc

你问题中的假设实现:

  • INT_MAX等于32767
  • INT_MIN等于-232+32768

是不可能的,因此不需要特别考虑。 INT_MIN将等于-INT_MAX-INT_MAX - 1。这是由C对整数类型的表示(6.2.6.2)所要求的,它需要n位作为值位,一位作为符号位,并且只允许一个单独的陷阱表示(不包括因填充位而无效的表示),即否则将表示负零/-INT_MAX - 1的那个。C++不允许任何超出C所允许的整数表示。

更新:微软的编译器显然没有注意到x > 10x >= 11测试相同的事情。它只有在x >= INT_MIN被替换为x > INT_MIN - 1u时才生成所需的代码,因为它可以将其检测为x <= INT_MAX(在这个平台上)的否定。

[向提问者(Nemo)的更新,详细说明我们下面的讨论]

我现在相信这个答案在所有情况下都有效,但原因较为复杂。我很可能会把奖励给这个解决方案,但为了以防万一,我想把所有的细节记录下来。

让我们从C++11的第18.3.3节开始:

表31描述了头文件<climits>

...

内容与标准C库头文件<limits.h>相同。

在这里,“标准C”指的是C99,其规范严格限制了有符号整数的表示方式。它们就像无符号整数一样,但其中一个比特位专门用于“符号”,零个或多个比特位专门用于“填充”。填充位不会对整数的值产生贡献,而符号位只作为二进制补码、反码或原码。
由于C++11继承了C99的宏,INT_MIN要么是-INT_MAX,要么是-INT_MAX-1,hvd的代码可以保证正常工作。(请注意,由于填充,INT_MAX可能远小于UINT_MAX/2...但由于有符号->无符号转换的工作方式,本答案可以很好地处理这个问题。)
C++03/C++98则更加棘手。它使用相同的措辞从“标准C”中继承,但现在“标准C”指的是C89/C90。
所有这些——C++98、C++03、C89/C90——都有我在问题中提到的措辞,但也包括这个(C++03第3.9.1段第7句)。
整数类型的表示应使用纯二进制计数系统来定义值。(44) [例如:此国际标准允许使用2的补码、1的补码和带符号幅度表示整数类型。]
脚注(44)定义了“纯二进制计数系统”:
一种用二进制数字0和1表示整数的位置表示法,其中由连续位表示的值是加性的,以1开头,并乘以连续的2的幂,除了最高位可能不是这样。
有趣的是,这个措辞自相矛盾,因为“纯二进制计数系统”的定义不允许使用符号/幅度表示!它确实允许高位具有例如值-2n-1(二进制补码)或-(2n-1-1)(一的补码)。但是没有一个高位的值会导致符号/幅度。
无论如何,根据这个定义,我的“假设实现”都不符合“纯二进制”,因此被排除在外。

然而,高位是特殊的事实意味着我们可以想象它贡献任何值:小正值、巨大正值、小负值或巨大负值。(如果符号位可以贡献-(2n-1-1),为什么不是-(2n-1-2)?等等。)

因此,让我们想象一种有符号整数表示法,将“符号”位分配给一个古怪的值。

符号位的小正值将导致int具有正范围(可能与unsigned一样大),并且hvd的代码可以很好地处理。

符号位的巨大正值将导致int具有比unsigned更大的最大值,这是被禁止的。

符号位的巨大负值将导致int表示非连续的值范围,规范中的其他措辞也排除了这种情况。

最后,如果有一个符号位贡献了一个小的负数,怎么样呢?我们可以让“符号位”中的1贡献-37到int值中吗?这样INT_MAX就是(比如)231-1,而INT_MIN就是-37?

这将导致一些数字有两种表示方式……但是补码给零两种表示方式,这是允许的,根据“示例”的规定。规范从未说过零是唯一可能有两种表示方式的整数。因此,我认为这个新的假设是符合规范的。

实际上,任何从-1到-INT_MAX-1的负值似乎都可以作为“符号位”的值,但不能更小(否则范围就不连续了)。换句话说,INT_MIN可以是从-INT_MAX-1到-1的任何值。

现在,猜猜看?为了避免hvd代码中的实现定义行为的第二次转换,我们只需要确保x - (unsigned)INT_MIN小于或等于INT_MAX。我们刚刚证明了INT_MIN至少为-INT_MAX-1。显然,x最多为UINT_MAX。将负数转换为无符号数与添加UINT_MAX+1相同。把它们放在一起:

x - (unsigned)INT_MIN <= INT_MAX

当且仅当

UINT_MAX - (INT_MIN + UINT_MAX + 1) <= INT_MAX
-INT_MIN-1 <= INT_MAX
-INT_MIN <= INT_MAX+1
INT_MIN >= -INT_MAX-1

最后一个例子我们刚刚展示过,所以即使在这种异常情况下,代码实际上也是有效的。

这就用尽了所有可能性,结束了这个极为学术的练习。

底线:C89/C90中的有符号整数存在一些严重未指定的行为,在C++98/C++03中继承了这种行为。 在C99中得到了修复,并通过从C99中包含<limits.h>来间接继承C++11的修复。 但是,即使是C++11也保留了自相矛盾的“纯二进制表示”措辞......


我同意 INT_MIN 的含义等是从 C 继承而来的。但这并不意味着 也是继承而来的。(实际上,由于每个实现都不同,它们怎么可能是继承而来的呢?)你推断 INT_MIN-INT_MAX 的范围内的推论依赖于在任何 C++ 规范中都没有出现的措辞。因此,虽然 C++ 确实继承了宏的语义含义,但规范并未提供(或继承)支持您推论的措辞。这似乎是 C++ 规范中的一个疏漏,阻止了完全符合要求的高效无符号到有符号转换。 - Nemo
我会说我的假设表示法是“纯二进制计数系统”。只是有点奇怪的偏见 :-). 但是,由于C++规范确实说<climits>具有与“标准C头文件<limits.h>”相同的内容,因此INT_MIN等必须对C有效。所以这就解决了C++11的问题(但不适用于C++03,因为“标准C”指的是C90)。我可能最终会接受这个答案。 - Nemo
6.2.6.2(2)明确指出,无符号表示中的某些值位可以是有符号表示中的填充位:“如果有M个值位在有符号类型中,N个值位在无符号类型中,则M≤N”。 (因此,N-M计算填充位加上符号位。)您可能会在C ++ 11中使用“纯二进制”:-INT_MAX不一定符合C规范中的INT_MIN的要求。 - Nemo
你介意我编辑你的回答以添加一些详细说明(参考资料等),包括我们在这里涵盖的一些引用吗?我希望问答内容正确,以防将来有人偶然发现。或者你更喜欢我只编辑我的问题? - Nemo
7
太棒了!不知道当时怎么错过了这个问题。 - Lightness Races in Orbit
显示剩余13条评论

20

这段代码只依赖于规范要求的行为,因此要满足要求(a)非常容易:

int unsigned_to_signed(unsigned n)
{
  int result = INT_MAX;

  if (n > INT_MAX && n < INT_MIN)
    throw runtime_error("no signed int for this number");

  for (unsigned i = INT_MAX; i != n; --i)
    --result;

  return result;
}

要满足(b)的要求并不容易。使用gcc 4.6.3 (-Os, -O2, -O3)和clang 3.0 (-Os, -O, -O2, -O3)编译将得出一个无操作。Intel 12.1.0则拒绝对此进行优化。我没有关于Visual C的信息。


1
好的,这太棒了。我希望我能将赏金分成80:20... 我怀疑编译器的推理是:如果循环不终止,则result会溢出;整数溢出是未定义的;因此循环终止;因此在终止时i == n;因此result等于n。我仍然更喜欢hvd的答案(对于不那么聪明的编译器的非病态行为),但这值得更多的赞。 - Nemo
1
无符号数被定义为模数。循环也有保证终止,因为 n 是某个无符号值,并且 i 最终必须到达每个无符号值。 - idupree

19
原始答案仅解决了将 unsigned 转换为 int 的问题。如果我们想要解决“某些无符号类型”到其相应的有符号类型的一般问题呢?此外,原始答案在引用标准部分和分析一些边角情况方面非常出色,但它并没有真正帮助我理解它为什么有效,因此这个答案将尝试给出一个强大的概念基础。这个答案将尝试解释“为什么”,并使用现代 C++ 特性来简化代码。

C++20 答案

随着 P0907:有符号整数是二进制补码 和被投票纳入 C++20 标准的 最终措辞 P1236,问题已经大大简化。现在,答案尽可能简单:

template<std::unsigned_integral T>
constexpr auto cast_to_signed_integer(T const value) {
    return static_cast<std::make_signed_t<T>>(value);
}

这就是它的全部。在这个问题中,使用static_cast(或C风格转换)最终被保证能够做到你需要的事情,而且许多程序员一直认为它总是这样做。
C++17的答案:
在C++17中,情况变得更加复杂。我们必须处理三种可能的整数表示方式(二进制补码、反码和原码)。即使在我们知道它必须是二进制补码的情况下,因为我们检查了可能值的范围,将超出有符号整数范围的值转换为该有符号整数仍然会给我们一个实现定义的结果。我们必须使用像其他答案中所见的技巧。
首先,这里是解决问题的通用代码:
template<typename T, typename = std::enable_if_t<std::is_unsigned_v<T>>>
constexpr auto cast_to_signed_integer(T const value) {
    using result = std::make_signed_t<T>;
    using result_limits = std::numeric_limits<result>;
    if constexpr (result_limits::min() + 1 != -result_limits::max()) {
        if (value == static_cast<T>(result_limits::max()) + 1) {
            throw std::runtime_error("Cannot convert the maximum possible unsigned to a signed value on this system");
        }
    }
    if (value <= result_limits::max()) {
        return static_cast<result>(value);
    } else {
        using promoted_unsigned = std::conditional_t<sizeof(T) <= sizeof(unsigned), unsigned, T>;
        using promoted_signed = std::make_signed_t<promoted_unsigned>;
        constexpr auto shift_by_window = [](auto x) {
            // static_cast to avoid conversion warning
            return x - static_cast<decltype(x)>(result_limits::max()) - 1;
        };
        return static_cast<result>(
            shift_by_window( // shift values from common range to negative range
                static_cast<promoted_signed>(
                    shift_by_window( // shift large values into common range
                        static_cast<promoted_unsigned>(value) // cast to avoid promotion to int
                    )
                )
            )
        );
    }
}


这篇回答中有比被采纳的回答更多的强制类型转换,这是为了确保编译器不会发出有符号/无符号不匹配警告并正确处理整数提升规则。
首先,我们针对不是二进制补码的系统有一个特殊情况(因此我们必须特别处理最大可能值,因为它没有任何映射)。之后,我们进入真正的算法。
第二个顶层条件很简单:我们知道该值小于或等于最大值,因此它适合于结果类型。即使有注释,第三个条件也比较复杂,因此一些示例可能有助于理解为什么每个语句都是必要的。
概念基础:数轴
首先,什么是“窗口”概念?考虑以下数轴:
   |   signed   |
<.........................>
          |  unsigned  |

原来对于二进制补码整数,可以将可以通过任一类型到达的数字线子集分为三个大小相等的类别:
- => signed only
= => both
+ => unsigned only

<..-------=======+++++++..>


这可以通过考虑表示来轻松证明。无符号整数从0开始,并使用所有位以2的幂增加值。所有位对于有符号整数完全相同,除了符号位,其值为-(2^position)而不是2^position。这意味着对于所有n-1位,它们表示相同的值。然后,无符号整数有一个更多的正常位,将总值翻倍(换句话说,设置该位与未设置该位的值一样多)。有关有符号整数的逻辑相同,只是具有该位设置的所有值都为负。
另外两个合法的整数表示,反码和补码,与二进制补码整数具有相同的所有值,除了最小的负值。 C ++根据可表示值的范围定义整数类型的所有内容,除了reinterpret_cast(和C ++ 20 std :: bit_cast )之外,而不是按位表示。这意味着只要我们不尝试创建陷阱表示,我们的分析就适用于这三种表示中的每一种。映射到此丢失值的无符号值是非常不幸的:刚好在无符号值的中间。幸运的是,我们的第一个条件检查(在编译时)是否存在这样的表示,并使用运行时检查特殊处理它。
第一个条件处理我们处于=部分的情况,这意味着我们处于重叠区域,在该区域中,可以在不更改值的情况下表示一种值为另一种值。代码中的shift_by_window 函数将所有值向下移动每个这些段的大小(我们必须减去最大值然后减1以避免算术溢出问题)。如果我们在该区域之外(我们在+区域中),我们需要向下跳一个窗口大小。这将使我们进入重叠范围,这意味着我们可以安全地从无符号转换为有符号,因为值没有变化。但是,我们还没有完成,因为我们已经将两个无符号值映射到每个有符号值。因此,我们需要向下移动到下一个窗口(-区域),以便再次具有唯一映射。

现在,这是否给我们提供了与问题中请求的模UINT_MAX + 1相一致的结果?UINT_MAX + 1等同于2^n,其中n是值表示中位数的数量。我们用于窗口大小的值等于2^(n-1)(序列中最后一个索引比大小少1)。我们将该值减去两次,这意味着我们将减去2 * 2^(n-1),它等于2^n。在算术模x中添加和减去x是无操作的,因此我们没有影响原始值模2^n

正确处理整数提升

因为这是一个通用函数,而不仅仅是intunsigned,所以我们还必须关注整数提升规则。有两种可能感兴趣的情况:一种是short小于int,另一种是shortint大小相同。

示例:short小于int

如果short小于int(在现代平台上很常见),则我们还知道unsigned short可以适合一个int中,这意味着对它的任何操作实际上会在int中发生,因此我们明确地将其转换为提升类型以避免这种情况。我们最终的语句非常抽象,如果我们替换实际值,就会变得更容易理解。对于我们的第一个有趣情况,不失一般性,让我们考虑一个16位的short和一个17位的int(这仍然是在新规则下允许的,并且只意味着这两个整数类型中至少有一个具有一些填充位):

constexpr auto shift_by_window = [](auto x) {
    return x - static_cast<decltype(x)>(32767) - 1;
};
return static_cast<int16_t>(
    shift_by_window(
        static_cast<int17_t>(
            shift_by_window(
                static_cast<uint17_t>(value)
            )
        )
    )
);

解决寻找最大的16位无符号值

constexpr auto shift_by_window = [](auto x) {
    return x - static_cast<decltype(x)>(32767) - 1;
};
return int16_t(
    shift_by_window(
        int17_t(
            shift_by_window(
                uint17_t(65535)
            )
        )
    )
);

简化为
return int16_t(
    int17_t(
        uint17_t(65535) - uint17_t(32767) - 1
    ) -
    int17_t(32767) -
    1
);

简化为

return int16_t(
    int17_t(uint17_t(32767)) -
    int17_t(32767) -
    1
);

简化为

return int16_t(
    int17_t(32767) -
    int17_t(32767) -
    1
);

简化为

return int16_t(-1);

我们输入最大可能的无符号数,得到-1,成功了!
例子:当short和int大小相同时
如果short与int大小相同(在现代平台上不常见),整型提升规则略有不同。在这种情况下,short提升为int,unsigned short提升为unsigned。幸运的是,我们将每个结果明确地转换为我们想要进行计算的类型,因此我们最终没有遇到任何问题的提升。为了不失一般性,让我们考虑一个16位的short和一个16位的int:
constexpr auto shift_by_window = [](auto x) {
    return x - static_cast<decltype(x)>(32767) - 1;
};
return static_cast<int16_t>(
    shift_by_window(
        static_cast<int16_t>(
            shift_by_window(
                static_cast<uint16_t>(value)
            )
        )
    )
);

解决最大可能的16位无符号值问题
auto x = int16_t(
    uint16_t(65535) - uint16_t(32767) - 1
);
return int16_t(
    x - int16_t(32767) - 1
);

简化为

return int16_t(
    int16_t(32767) - int16_t(32767) - 1
);

简化为

return int16_t(-1);

我们输入最大可能的无符号数,得到 -1,表示成功!

如果我只关心 intunsigned,而不关心警告,像原来的问题一样怎么办?

constexpr int cast_to_signed_integer(unsigned const value) {
    using result_limits = std::numeric_limits<int>;
    if constexpr (result_limits::min() + 1 != -result_limits::max()) {
        if (value == static_cast<unsigned>(result_limits::max()) + 1) {
            throw std::runtime_error("Cannot convert the maximum possible unsigned to a signed value on this system");
        }
    }
    if (value <= result_limits::max()) {
        return static_cast<int>(value);
    } else {
        constexpr int window = result_limits::min();
        return static_cast<int>(value + window) + window;
    }
}

实时查看

https://godbolt.org/z/74hY81

在这里,我们可以看到clang、gcc和icc在-O2-O3下不会为castcast_to_signed_integer_basic生成任何代码,而MSVC在/O2下也不会生成任何代码,因此解决方案是最优的。


3
你可以直接告诉编译器你想要做什么:
int unsigned_to_signed(unsigned n) {
  if (n > INT_MAX) {
    if (n <= UINT_MAX + INT_MIN) {
      throw "no result";
    }
    return static_cast<int>(n + INT_MIN) - (UINT_MAX + INT_MIN + 1);
  } else {
    return static_cast<int>(n);
  }
}

使用gcc 4.7.2编译x86_64-linux的代码,命令为g++ -O -S test.cpp,生成的结果如下:

_Z18unsigned_to_signedj:
    movl    %edi, %eax
    ret

UINT_MAX 是一个 unsigned int 类型的表达式,这使得你整个的 static_cast<int>(n + INT_MIN) - (UINT_MAX + INT_MIN + 1) 也是该类型。不过应该可以修复这个问题,并且我期望它仍然能够编译成功。 - user743382

2
如果x是我们的输入...
如果x > INT_MAX,我们希望找到一个常数k,使得0 < x - k*INT_MAX < INT_MAX
这很容易 - unsigned int k = x / INT_MAX;。然后,让unsigned int x2 = x - k*INT_MAX; 现在,我们可以安全地将x2转换为int。让int x3 = static_cast<int>(x2); 现在,如果k > 0,我们想从x3中减去类似于UINT_MAX - k * INT_MAX + 1的东西。
现在,在一个二进制补码系统中,只要x > INT_MAX,这就相当于:
unsigned int k = x / INT_MAX;
x -= k*INT_MAX;
int r = int(x);
r += k*INT_MAX;
r -= UINT_MAX+1;

请注意,在C++中,UINT_MAX+1保证为零,将其转换为int是无操作的,我们从中减去了k*INT_MAX,然后又加回了“相同的值”。因此,一个可接受的优化器应该能够消除所有这些愚蠢的东西!
这就留下了x > INT_MAX是否成立的问题。我们创建了两个分支,一个带有x > INT_MAX,一个没有。没有分支执行直接强制类型转换,编译器将其优化为无操作。有分支执行后,优化器也会将其优化为无操作。聪明的优化器意识到两个分支的结果相同,并删除其中一个分支。
问题:如果UINT_MAX相对于INT_MAX非常大,则上述方法可能不起作用。我假设k*INT_MAX <= UINT_MAX+1是隐含的。
我们可以使用一些枚举来解决这个问题:
enum { divisor = UINT_MAX/INT_MAX, remainder = UINT_MAX-divisor*INT_MAX };

这些计算是基于2s补码系统的,我相信结果会是2和1(我们能保证这个数学公式有效吗?这很棘手...),并且可以根据这些逻辑进行操作,在非2s补码系统上轻松优化掉...

这也打开了异常情况。只有当UINT_MAX比(INT_MIN-INT_MAX)大得多时才可能发生,因此您可以在if块中询问确切的问题并将异常代码放入其中,而在传统系统上不会减慢您的速度。

我不确定如何构建那些编译时常量以正确处理它。


UINT_MAX 不能相对于 INT_MAX 较小,因为规范保证每个正有符号整数都可以表示为无符号整数。但是在每个系统上,UINT_MAX+1 都为零;无符号算术始终是模 UINT_MAX+1 的。尽管如此,这里可能有一个可行的方法的核心... - Nemo
@Nemo,我在关注这个线程,所以请原谅我可能会问一个显而易见的问题:你的陈述“UINT_MAX+1在每个系统上都是零”是否在'03规范中得到确认?如果是,那么我应该在哪个具体的子章节下查找?谢谢。 - WhozCraig
@WhozCraig: 第3.9.1节第4段:“声明为无符号的无符号整数应遵守算术模2^n的规律,其中n是该特定大小的整数值表示中的位数”,并带有一个脚注,指出“这意味着无符号算术不会溢出,因为不能由结果无符号整数类型表示的结果将对可以由结果无符号整数类型表示的最大值加1取模。”基本上,无符号被指定为按您想要/期望的方式工作。 - Nemo

1

std::numeric_limits<int>::is_modulo 是一个编译时常量,因此您可以将其用于模板特化。问题解决了,至少如果编译器支持内联的话。

#include <limits>
#include <stdexcept>
#include <string>

#ifdef TESTING_SF
    bool const testing_sf = true;
#else
    bool const testing_sf = false;
#endif

// C++ "extensions"
namespace cppx {
    using std::runtime_error;
    using std::string;

    inline bool hopefully( bool const c ) { return c; }
    inline bool throw_x( string const& s ) { throw runtime_error( s ); }

}  // namespace cppx

// C++ "portability perversions"
namespace cppp {
    using cppx::hopefully;
    using cppx::throw_x;
    using std::numeric_limits;

    namespace detail {
        template< bool isTwosComplement >
        int signed_from( unsigned const n )
        {
            if( n <= unsigned( numeric_limits<int>::max() ) )
            {
                return static_cast<int>( n );
            }

            unsigned const u_max = unsigned( -1 );
            unsigned const u_half = u_max/2 + 1;

            if( n == u_half )
            {
                throw_x( "signed_from: unsupported value (negative max)" );
            }

            int const i_quarter = static_cast<int>( u_half/2 );
            int const int_n1 = static_cast<int>( n - u_half );
            int const int_n2 = int_n1 - i_quarter;
            int const int_n3 = int_n2 - i_quarter;

            hopefully( n == static_cast<unsigned>( int_n3 ) )
                || throw_x( "signed_from: range error" );

            return int_n3;
        }

        template<>
        inline int signed_from<true>( unsigned const n )
        {
            return static_cast<int>( n );
        }
    }    // namespace detail

    inline int signed_from( unsigned const n )
    {
        bool const is_modulo = numeric_limits< int >::is_modulo;
        return detail::signed_from< is_modulo && !testing_sf >( n );
    }
}    // namespace cppp

#include <iostream>
using namespace std;
int main()
{
    int const x = cppp::signed_from( -42u );
    wcout << x << endl;
}


编辑:修复代码以避免在非模块化整数机器上可能出现的陷阱(已知只有一种,即古老配置版本的Unisys Clearpath)。为了简单起见,在这种机器上不支持值-2n-1,其中nint值位数,实际上该值也不会被机器支持(即使用符号和幅度或1的补码表示)。


1

我认为使用memcpy是可行的。任何一个好的编译器都会对其进行优化:

#include <stdio.h>
#include <memory.h>
#include <limits.h>

static inline int unsigned_to_signed(unsigned n)
{
    int result;
    memcpy( &result, &n, sizeof(result));
    return result;
}

int main(int argc, const char * argv[])
{
    unsigned int x = UINT_MAX - 1;
    int xx = unsigned_to_signed(x);
    return xx;
}

对于我来说(Xcode 8.3.2,Apple LLVM 8.1,-O3),这将产生:
_main:                                  ## @main
Lfunc_begin0:
    .loc    1 21 0                  ## /Users/Someone/main.c:21:0
    .cfi_startproc
## BB#0:
    pushq    %rbp
Ltmp0:
    .cfi_def_cfa_offset 16
Ltmp1:
    .cfi_offset %rbp, -16
    movq    %rsp, %rbp
Ltmp2:
    .cfi_def_cfa_register %rbp
    ##DEBUG_VALUE: main:argc <- %EDI
    ##DEBUG_VALUE: main:argv <- %RSI
Ltmp3:
    ##DEBUG_VALUE: main:x <- 2147483646
    ##DEBUG_VALUE: main:xx <- 2147483646
    .loc    1 24 5 prologue_end     ## /Users/Someone/main.c:24:5
    movl    $-2, %eax
    popq    %rbp
    retq
Ltmp4:
Lfunc_end0:
    .cfi_endproc

1
这并没有回答问题,因为标准并不保证无符号数的二进制表示与有符号数的表示匹配。 - TLW

1

我被诅咒了,必须使用默认配置为“-mint8”的6809编译器,其中int为8位 :-((这是Vectrex的开发环境)。long为2个字节,long long为4个字节,而我不知道short是什么... - Graham Toal
@GrahamToal - 您所描述的不是符合标准的 C 实现。C 要求 intshort 至少包含 16 位信息。 - owacoder
因此被称为“诅咒”。我很清楚这样做是很愚蠢的,我已经与负责这个决定的人争论过,但它不会改变,支持库是写在这个基础上的,所以关掉它也不现实。以下是gcc6809文档中对此的描述(为了简洁起见):“int”为16位宽。“short”或“char”均为8位。 “long”为32位,即4字节宽。 可选地,您可以使用-mint8命令行选项使整数宽度为8位。这也将缩短“long”的大小为16位。 它不影响“short”或“char”。 - Graham Toal

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