【问题标题】:Efficient unsigned-to-signed cast avoiding implementation-defined behavior高效的无符号到有符号转换,避免实现定义的行为
【发布时间】:2012-10-20 11:14:58
【问题描述】:

我想定义一个函数,它接受 unsigned int 作为参数并返回一个 int 同余模 UINT_MAX+1 到参数。

第一次尝试可能如下所示:

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

但正如任何语言律师所知,对于大于 INT_MAX 的值,从无符号转换为有符号是实现定义的。

我想实现这一点,以便 (a) 它仅依赖于规范规定的行为; (b) 它可以在任何现代机器上编译成无操作并优化编译器。

至于奇怪的机器......如果没有有符号的整数与无符号整数模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&lt;int&gt;::is_modulo 是真的
  • 将 unsigned n 转换为 int 会保留 0 零

在这个假设的实现中,每个unsigned 值都有一个int 值全等(mod UINT_MAX+1)。所以我的问题会很明确。

我声称这个假设的 C++ 实现完全符合 C++98、C++03 和 C++11 规范。我承认我没有记住所有的每一个字……但我相信我已经仔细阅读了相关部分。因此,如果您希望我接受您的回答,您要么必须 (a) 引用排除此假设实现的规范,要么 (b) 正确处理它。

确实,正确答案必须处理标准允许的每一个假设实现。根据定义,这就是“仅调用标准强制行为”的含义。

顺便提一下,std::numeric_limits&lt;int&gt;::is_modulo 在这里完全没用,原因有很多。一方面,它可以是true,即使无符号到有符号的强制转换不适用于大的无符号值。另一方面,它可以是true,即使在一个补码或符号幅度系统上,如果算术只是对整个整数范围取模。等等。如果你的答案依赖于is_modulo,那就错了。

[更新 2]

hvd's answer 教会了我一些东西:我对整数的假设 C++ 实现为现代 C 所允许。C99 和 C11 标准对有符号整数的表示非常具体;实际上,它们只允许补码、补码和符号幅度(第 6.2.6.2 节第 (2) 节;)。

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

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

对于每个有符号整数类型,都有一个对应的(但 不同的)无符号整数类型(用关键字指定 无符号),它使用相同的存储量(包括符号 信息)并具有相同的对齐要求。的范围 有符号整数类型的非负值是 对应的无符号整数类型,以及 每种类型的相同值都是相同的。

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=-232+32768 是允许的。如果您的答案假设不是这样,那么除非您引用 C++ 标准证明我错了,否则它是不正确的。

【问题讨论】:

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

标签: c++ casting integer language-lawyer integer-overflow


【解决方案1】:

扩展 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 &gt;= INT_MIN(记住提升规则,INT_MIN 被转换为unsigned),然后是x - INT_MIN &lt;= INT_MAX,所以这不会有任何溢出。

如果这不明显,请查看声明“如果 x &gt;= -4u,则 x + 4 &lt;= 3。”,并记住 INT_MAX 至少等于 -INT_MIN - 1 的数学值.

在最常见的系统上,!(x &lt;= INT_MAX) 暗示 x &gt;= 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 &gt; 10x &gt;= 11 测试相同的东西。如果x &gt;= INT_MIN 被替换为x &gt; INT_MIN - 1u,它只会生成所需的代码,它可以将其检测为x &lt;= INT_MAX 的否定(在此平台上)。

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

我现在相信这个答案适用于所有情况,但原因很复杂。我很可能会奖励这个解决方案,但我想捕捉所有血淋淋的细节以防万一。

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

表 31 描述了标头 &lt;climits&gt;

...

内容与标准C库头&lt;limits.h&gt;相同。

这里,“标准 C”是指 C99,其规范严格限制了有符号整数的表示。它们就像无符号整数,但有一位专用于“符号”,零位或多位专用于“填充”。填充位对整数的值没有贡献,符号位仅作为二进制补码、二进制补码或符号大小起作用。

由于 C++11 从 C99 继承了 &lt;climits&gt; 宏,因此 INT_MIN 是 -INT_MAX 或 -INT_MAX-1,并且 hvd 的代码保证可以工作。 (请注意,由于填充,INT_MAX 可能比 UINT_MAX/2 小得多......但是由于有符号->无符号转换的工作方式,这个答案处理得很好。)

C++03/C++98 比较复杂。它使用相同的措辞从“标准 C”继承 &lt;climits&gt;,但现在“标准 C”表示 C89/C90。

所有这些——C++98、C++03、C89/C90——都有我在我的问题中给出的措辞,但也包括这个(C++03 第 3.9.1 节第 7 段):

整数类型的表示应通过使用 纯二进制计数系统。(44) [示例:this International 标准允许 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?

这将导致某些数字具有两种表示形式...但是补码将两种表示形式归零,根据“示例”,这是允许的。规范中没有任何地方说零是可能有两种表示形式的 only 整数。所以我认为这个新假设是规范允许的。

确实,从 -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

最后是我们刚刚展示的,所以即使在这种反常的情况下,代码也确实有效。

这耗尽了所有的可能性,从而结束了这个极其学术的练习。

底线:在 C++98/C++03 继承的 C89/C90 中有符号整数存在一些严重未指定的行为。它在 C99 中已修复,C++11 通过合并 C99 中的 &lt;limits.h&gt; 间接继承了该修复。但即使是 C++11 也保留了自相矛盾的“纯二进制表示”措辞……

【讨论】:

  • 问题已更新。我(现在)对这个答案投反对票以阻止其他人......我稍后会取消投票,因为答案很有趣。 (对于 C 是正确的,但对于 C++ 是错误的。我认为。)
  • @Nemo 在这种情况下,C 标准适用于 C++;至少,&lt;limits.h&gt; 中的值在 C++ 标准中定义为与 C 标准中的含义相同,因此 C 对 INT_MININT_MAX 的所有要求都在 C++ 中继承。您是正确的,C++03 指的是 C90,C90 对允许的整数表示含糊不清,但是 C99 更改(至少通过 C++11 通过&lt;limits.h&gt; 继承,希望也以更直接的方式)将其限制在这三个是编纂现有实践的一个:不存在其他实现。
  • 我同意INT_MIN等的含义是从C继承的。但这并不意味着是。 (事实上​​,他们怎么可能,因为每个实现都不同?)您对INT_MIN-INT_MAX 之内的推断取决于任何 C++ 规范中根本没有出现的措辞。因此,尽管 C++ 确实继承了宏的语义含义,但规范并未提供(或继承)支持您的推理的措辞。这似乎是 C++ 规范中的一个疏忽,它阻止了完全符合有效的无符号到有符号转换。
  • @Nemo 如果您(也许是正确的)声称 C++ 允许其他表示,那么在这样的实现中,我声称 INT_MIN 不需要 是最小的int 类型的可表示值,因为就 C 而言,如果类型不符合 int 的要求,C 标准不可能以任何方式涵盖该实现,并且 C++ 标准不提供任何除了“C标准所说的”之外的定义。我会检查是否有更直接的解释。
  • 这太棒了。不知道我当时是怎么错过这个问题的。
【解决方案2】:

此代码仅依赖于规范规定的行为,因此很容易满足要求 (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 的信息。

【讨论】:

  • 好的,这太棒了。我希望我可以在 80:20 分赏金......我怀疑编译器的推理是:如果循环没有终止,result 溢出;整数溢出未定义;因此循环终止;因此 i == n 在终止时;因此result 等于n。我仍然更喜欢 hvd 的答案(对于不太聪明的编译器上的非病态行为),但这值得更多的投票。
  • 无符号被定义为模数。循环也保证终止,因为n 是一些无符号值,i 最终必须到达每个无符号值。
【解决方案3】:

原始答案仅解决了unsigned => int 的问题。如果我们想将“一些无符号类型”的一般问题解决到其对应的有符号类型怎么办?此外,原始答案在引用标准部分和分析一些极端案例方面非常出色,但它并没有真正帮助我了解它为什么起作用,所以这个答案将试图提供一个强有力的概念基础。此答案将尝试帮助解释“为什么”,并使用现代 C++ 功能尝试简化代码。

C++20 答案

P0907: Signed Integers are Two’s Complement 和被选入 C++20 标准的final wording 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
                    )
                )
            )
        );
    }
}

这比接受的答案多一些,这是为了确保没有来自编译器的有符号/无符号不匹配警告并正确处理整数提升规则。

对于不是二进制补码的系统,我们首先有一个特殊情况(因此我们必须特别处理最大可能值,因为它没有任何要映射的内容)。之后,我们就进入了真正的算法。

第二个顶级条件很简单:我们知道该值小于或等于最大值,因此它适合结果类型。即使使用 cmets,第三个条件也稍微复杂一些,因此一些示例可能有助于理解为什么每个语句都是必要的。

概念基础:数轴

首先,window 是什么概念?考虑以下数轴:

   |   signed   |
<.........................>
          |  unsigned  |

事实证明,对于二进制补码整数,您可以将任一类型可以到达的数轴子集划分为三个大小相等的类别:

- => signed only
= => both
+ => unsigned only

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

这可以通过考虑表示来轻松证明。无符号整数从 0 开始,并使用所有位来增加 2 的幂的值。有符号整数对于除符号位之外的所有位都是完全相同的,它的价值是 -(2^position) 而不是 @987654334 @。这意味着对于所有n - 1 位,它们代表相同的值。然后,无符号整数多了一个普通位,这使值的总数加倍(换句话说,设置了该位的值与未设置的值一样多)。相同的逻辑适用于有符号整数,除了设置该位的所有值都是负数。

另外两个合法的整数表示,一个的补码和符号幅度,除了一个:最负值之外,都具有与二进制补码整数相同的值。除了 reinterpret_cast(和 C++20 std::bit_cast)之外,C++ 定义了关于整数类型的所有内容,是根据可表示值的范围,而不是根据位表示。这意味着只要我们不尝试创建陷阱表示,我们的分析将适用于这三种表示中的每一种。映射到这个缺失值的无符号值是一个相当不幸的值:位于无符号值中间的那个。幸运的是,我们的第一个条件(在编译时)检查这种表示是否存在,然后通过运行时检查对其进行特殊处理。

第一个条件处理我们在= 部分的情况,这意味着我们处于重叠区域,其中一个中的值可以在另一个中表示而无需更改。代码中的shift_by_window 函数将所有值向下移动每个段的大小(我们必须减去最大值然后减去 1 以避免算术溢出问题)。如果我们在那个区域之外(我们在+ 区域),我们需要向下跳一个窗口大小。这使我们处于重叠范围,这意味着我们可以安全地从无符号转换为有符号,因为值没有变化。但是,我们还没有完成,因为我们已经将两个无符号值映射到每个有符号值。因此,我们需要向下移动到下一个窗口(@98​​7654341@ 区域),以便我们再次拥有唯一的映射。

现在,这是否为我们提供了问题中要求的结果全等 mod UINT_MAX + 1UINT_MAX + 1 等价于2^n,其中n 是值表示中的位数。我们用于窗口大小的值等于2^(n - 1)(值序列中的最终索引比大小小一)。我们将该值减去两次,这意味着我们减去2 * 2^(n - 1),等于2^n。加减x是算术模x中的空操作,所以我们没有影响原值模2^n

正确处理整数促销

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

示例: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,成功!

示例:shortint 大小相同

如果shortint 大小相同(在现代平台上不常见),则积分提升规则略有不同。在这种情况下,short 提升为 intunsigned 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处没有生成代码,所以解决方案是最优的。

【讨论】:

    【解决方案4】:

    你可以明确告诉编译器你想做什么:

    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_MAXunsigned int 类型的表达式,这使您的整个static_cast&lt;int&gt;(n + INT_MIN) - (UINT_MAX + INT_MIN + 1) 都属于该类型。不过,应该可以解决这个问题,我希望它仍然可以编译。
    【解决方案5】:

    如果x 是我们的输入...

    如果x &gt; 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&lt;int&gt;(x2);

    如果k &gt; 0,我们现在要从x3 中减去UINT_MAX - k * INT_MAX + 1 之类的东西。

    现在,在 2s 补码系统上,只要x &gt; INT_MAX,就可以:

    unsigned int k = x / INT_MAX;
    x -= k*INT_MAX;
    int r = int(x);
    r += k*INT_MAX;
    r -= UINT_MAX+1;
    

    请注意,UINT_MAX+1 在 C++ 中保证为零,转换为 int 是一个 noop,我们减去 k*INT_MAX 然后将其添加回“相同的值”。所以一个可接受的优化器应该能够消除所有这些愚蠢的事情!

    这就留下了x &gt; INT_MAX 与否的问题。好吧,我们创建了 2 个分支,一个带有 x &gt; INT_MAX,一个没有。没有的会进行严格转换,编译器会将其优化为 noop。带有 ... 的那个在优化器完成后执行 noop。智能优化器将两个分支实现为同一事物,然后删除分支。

    问题:如果UINT_MAX 相对于INT_MAX 确实很大,则上述方法可能不起作用。我假设k*INT_MAX &lt;= 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 只是关注这个线程,所以请原谅我可能显而易见的问题:您的声明“UINT_MAX+1 在 '03-spec 中建立的每个系统上都是零吗?如果是这样,是否有特定的我应该看下面的小节吗?谢谢。
    • @WhozCraig:第 3.9.1 节第 4 段:“声明为无符号的无符号整数应遵守算术模 2^n 的定律,其中 n 是该特定大小的值表示中的位数整数”,脚注说“这意味着无符号算术不会溢出,因为不能由得到的无符号整数类型表示的结果以比结果可以表示的最大值大一的数字为模减少无符号整数类型。”基本上 unsigned 被指定为按照您想要/期望的方式工作。
    • @Nemo 谢谢。非常感谢。
    【解决方案6】:

    std::numeric_limits&lt;int&gt;::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;
    }
    


    编辑:修正了代码以避免在非模块化 int 机器上可能出现的陷阱(已知只有一个存在,即 Unisys Clearpath 的过时配置版本)。为简单起见,这是通过不支持值 -2n-1 来完成的,其中 nint 值位的数量,在这样的机器(即,在 Clearpath 上)。实际上,机器也不支持该值(即,使用符号和大小或 1 的补码表示)。

    【讨论】:

      【解决方案7】:

      我认为int类型至少是两个字节,所以INT_MIN和INT_MAX在不同平台上可能会有所不同。

      Fundamental types

      ≤climits≥ header

      【讨论】:

      • 我被诅咒为 6809 使用编译器,默认配置为“-mint8”,其中 int 为 8 位 :-((这是 Vectrex 的开发环境)long 为 2字节,long long 是 4 个字节,我不知道 short 是什么......
      • @GrahamToal - 您所描述的不是符合标准的 C 实现。 C 要求 intshort 包含至少 16 位信息。
      • 因此出现了“被诅咒”这个词。我很清楚这是一件愚蠢的事情,我已经与负责此决定的人争论过,但它不会改变,并且支持库是在假设这一点的情况下编写的,因此关闭它也不实用.以下是 gcc6809 文档中的描述:(缩短以适应)“int”为 16 位宽。 “short”或“char”都是 8 位。 “long”是 32 位或 4 字节宽。或者,您可以使用 -mint8 命令行选项将整数设为 8 位宽。这也将“long”的大小缩短为 16 位。它不会影响“short”或“char”。
      【解决方案8】:

      我的钱花在了使用 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
      

      【讨论】:

      • 这不能回答问题,因为标准保证无符号的二进制表示与有符号表示相匹配。
      猜你喜欢
      • 2013-09-30
      • 1970-01-01
      • 1970-01-01
      • 2011-10-25
      • 1970-01-01
      • 2012-11-18
      • 2017-04-22
      • 2011-04-19
      • 1970-01-01
      相关资源
      最近更新 更多