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


93

我想定义一个函数,该函数接受unsigned intas作为参数,int并向该参数返回全模UINT_MAX + 1。

第一次尝试可能看起来像这样:

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

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

我要实现这一点,以便(a)仅依赖规范要求的行为;(b)它可以在任何现代机器和优化的编译器上编译为no-op。

至于奇异的机器...如果没有符号的int与未签名的int模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年的某个假设系统上,它可以做谁知道什么。所以说我要避免这种情况。

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

回顾一下,我想:

  • 从unsigned int转换为signed int
  • 保留值mod UINT_MAX + 1
  • 仅调用标准规定的行为
  • 使用优化的编译器在典型的二进制补码机上编译为无操作

[更新]

让我举一个例子来说明为什么这不是一个小问题。

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

  • sizeof(int) 等于4
  • sizeof(unsigned) 等于4
  • INT_MAX 等于32767
  • INT_MIN等于-2 32 + 32768
  • UINT_MAX等于2 32 - 1
  • 算术上int是模2 32(进入范围INT_MIN通过INT_MAX
  • std::numeric_limits<int>::is_modulo 是真的
  • 将unsigned强制转换n为int会保留0 <= n <= 32767的值,否则返回

在这种假设的实现方式中,int每个unsigned值恰好有一个全等值(mod 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节(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与未符号整数的模UINT_MAX + 1模数相等,那么我想抛出一个异常。” 也就是说,我希望“正确的”签名的int(如果存在)。如果它不存在(例如在填充位或补码表示形式中可能发生的情况),那么我想检测到该情况并针对转换的特定调用进行处理。
Nemo

抱歉,不确定我怎么想的。
史蒂夫·杰索普

顺便说一句,我认为在您假定的棘手实现中int,至少需要33位来表示它。我知道这只是一个脚注,因此您可以辩称它是非规范性的,但是我认为C ++ 11中的脚注49的意图是正确的(因为它是标准中使用的术语的定义),因此并不矛盾规范文本中明确指出的任何内容。因此,所有负值都必须由设置了最高位的位模式来表示,因此不能2^32 - 32768将它们填充为32位。并不是说您的论点完全取决于的大小int
史蒂夫·杰索普

至于您对hvd答案的修改,我认为您对注释49的理解有误。您说符号幅度是禁止的,但事实并非如此。您将其读为:“连续位表示的值是加法的,以1开头,并且(乘以连续的2的整数次幂,也许是位置最高的位除外)”。我认为应该读为“除连续位最高的位外,用连续位表示的值(加法,从1开始,再乘以连续的2的整数次幂)”。也就是说,如果设置了高位,则所有投注均关闭。
史蒂夫·杰索普

@SteveJessop:您的解释可能是正确的。如果是这样,它确实排除了我的假设...但是它也引入了真正大量的可能性,这使得这个问题很难回答。在我看来,这实际上像是规范中的错误。(显然,C委员会是这样认为的,并在C99中彻底修改了它。我想知道为什么C ++ 11没有采用他们的方法?)
Nemo

Answers:


70

扩展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,这样就不会有任何溢出。

如果不是很明显,请查看声明“ If x >= -4u,then 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等于-2 32 + 32768

这是不可能的,因此不需要特殊考虑。INT_MIN将等于-INT_MAX-INT_MAX - 1。这是基于C的整数类型表示形式(6.2.6.2),其中要求n位为值位,一位为符号位,并且仅允许一个陷阱​​陷阱表示(不包括由于填充位而无效的表示),即否则表示负零/的1 -INT_MAX - 1。C ++不允许C所允许的任何整数表示形式。

更新:微软的编译器显然没有注意到x > 10x >= 11测试相同的东西。如果x >= INT_MIN将替换为x > INT_MIN - 1u,它只会生成所需的代码,它可以检测为否定x <= INT_MAX(在此平台上)。

[发问者(尼莫)的更新,详细说明了我们在下面的讨论]

我现在认为,这个答案在所有情况下都适用,但是原因很复杂。我很可能会奖励这种解决方案,但我想捕获所有血腥细节,以防有人在意。

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

表31描述了标题<climits>

...

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

此处,“标准C”表示C99,其规格严重限制了有符号整数的表示。它们就像无符号整数一样,但是一位专用于“符号”,而零位或更多位专用于“填充”。填充位不影响整数值,而符号位仅以二进制补码,1补码或符号量级起作用。

由于C ++ 11<climits>从C99继承了宏,因此INT_MIN是-INT_MAX或-INT_MAX-1,并且可以确保hvd的代码正常工作。(请注意,由于填充的原因,INT_MAX可能比UINT_MAX / 2少得多...但是由于有符号->无符号强制转换的工作方式,此答案可以很好地解决此问题。)

C ++ 03 / C ++ 98比较棘手。它使用相同的措词继承<climits>自“ Standard C”,但是现在“ Standard C”表示C89 / C90。

所有这些-C ++ 98,C ++ 03,C89 / C90-在我的问题中都给出了措辞,但也包括以下内容(C ++ 03第3.9.1节第7段):

整数类型的表示应使用纯二进制计算系统来定义值。(44)[示例:本国际标准允许整数类型的2的补码,1的补码和有符号的幅度表示。]

脚注(44)定义了“纯二进制计算系统”:

使用二进制数字0和1的整数的位置表示,其中连续位表示的值是加法的,从1开始,然后乘以连续的2的整数次方,可能是位置最高的位。

这个措辞的有趣之处在于它自相矛盾,因为“纯二进制计算系统”的定义不允许使用符号/大小表示!它确实允许高位具有值-2 n-1(二进制补码)或-(2 n-1 -1)(二进制补码)。但是,没有高位值会导致符号/幅值。

无论如何,在此定义下,我的“假设实现”不符合“纯二进制”的条件,因此将其排除在外。

但是,高位是一个特殊的事实,这意味着我们可以想象它根本贡献了任何价值:小的正值,巨大的正值,小的负值或巨大的负值。(如果符号位可以贡献-(2 n-1 -1),为什么不提供-(2 n-1 -2)?等等。)

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

符号位的正值较小会导致的正值范围int(可能与一样大unsigned),而hvd的代码也可以正常处理。

符号位的正值太大会导致int最大值大于unsigned,这是禁止的。

符号位的巨大负值将导致int表示不连续的值范围,并且规范中的其他措辞也将其排除在外。

最后,符号位对负值的贡献很小吗?我们能否在“符号位”中加1,例如,对int的值贡献-37?那么INT_MAX是(例如)2 31 -1,而INT_MIN是-37?

这将导致某些数字具有两个表示形式...但是,ones-complement会将两个表示形式设为零,这是根据“示例”允许的。规范没有地方说零是唯一可能具有两种表示形式的整数。因此,我认为规范允许这种新的假设。

实际上,从-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。将负数强制转换为unsigned与添加相同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中已修复,而C ++ 11通过<limits.h>从C99合并而间接继承了此修复。但是,即使C ++ 11仍保留了自相矛盾的“纯二进制表示”的用语...


问题已更新。我暂时拒绝对此答案,以阻止其他人……由于答案很有趣,我稍后将取消对此答案的投票。(对于C语言是正确的,但对于C ++是错误的。我认为。)
Nemo 2012年

@Nemo在这种情况下,C标准适用于C ++。至少,<limits.h>C ++标准中定义的in具有与C标准相同的含义,因此C的所有要求INT_MININT_MAXC继承于C ++。您是正确的,C ++ 03引用了C90,并且C90对于允许的整数表示形式含糊不清,但是C99更改(至少通过<limits.h>C ++ 11继承,希望也可以采用更直接的方式)将其限制为这三个是将现有做法编纂而成的:没有其他实现。

我同意INT_MINetc的含义是从C继承的。但这并不意味着这些是。(实际上,由于每个实现都是不同的,所以它们怎么可能呢?)INT_MIN1之内的推论-INT_MAX取决于根本没有出现在任何C ++规范中的措辞。因此,尽管C ++确实继承了宏的语义,但规范并未提供(或继承)支持您推断的措辞。这似乎是C ++规范中的一个疏漏,它阻止了完全一致的有效无符号到符号的强制转换。
Nemo 2012年

@Nemo如果您(也许是正确的)声称C ++允许其他表示形式,则在这样的实现上,我声称INT_MIN 并不需要是type的最小可表示值int,因为就C而言,如果类型不是为了满足的要求int,C标准可能无法以任何方式涵盖该实现,并且C ++标准除了“ C标准所说的内容”之外没有提供任何定义。我将检查是否有更直接的解释。

7
好漂亮 不知道我当时怎么错过这个问题。
Lightness Races Orbit在2013年

17

该代码仅依赖于规范规定的行为,因此很容易满足要求(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)编译为无操作。英特尔12.1.0拒绝对此进行优化。而且我没有有关Visual C的信息。


1
好,这太棒了 我希望我可以将赏金分配给80:20 result。整数溢出未定义;因此循环终止;因此i == n在终止时;因此result等于n。我仍然必须更喜欢hvd的答案(针对不太聪明的编译器的非病理行为),但这值得更多的投票。
Nemo 2012年

1
无符号定义为模。循环也可以保证终止,因为它n是一些无符号的值,i最终必须达到每个无符号的值。
idupree

7

原始答案仅针对unsigned=>解决了问题int。如果我们想将“某些无符号类型”的一般问题解决为其对应的带符号类型,该怎么办?此外,原始答案在引用该标准的各个部分并分析某些极端情况时非常出色,但是它并不能真正帮助我理解它的工作原理,因此,该答案将为您提供坚实的概念基础。该答案将尝试帮助解释“为什么”,并使用现代C ++功能来简化代码。

C ++ 20答案

P0907极大地简化了该问题:带符号整数是Two的补码最后的措词P1236被投票支持C ++ 20标准。现在,答案尽可能简单:

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
                    )
                )
            )
        );
    }
}

它比可接受的答案多强制转换,这是为了确保编译器没有任何有符号/无符号不匹配警告,并正确处理整数提升规则。

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

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

概念基础:数字线

首先,这个window概念是什么?考虑以下数字行:

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

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

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

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

通过考虑表示可以很容易地证明这一点。一个无符号整数从处开始,0并使用所有位以2的幂为单位增加值。除了符号位(值得-(2^position)代替)之外,所有其他位的有符号整数完全相同2^position。这意味着对于所有n - 1位,它们表示相同的值。然后,无符号整数再增加一个普通位,这使值的总数增加了一倍(换句话说,设置该位的值与未设置该位的值一样多)。除了带该位设置的所有值均为负之外,带符号整数的逻辑相同。

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

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

现在,这是否给我们提供了结果一致的mod UINT_MAX + 1,如问题中所要求的?UINT_MAX + 1等效于2^n,其中n是值表示形式中的位数。我们用于窗口大小的值等于2^(n - 1)(值序列中的最后一个索引比大小小1)。我们将该值减去两次,这意味着我们减去2 * 2^(n - 1)等于的值2^n。加法和减法x在算术mod中是不可操作的x,因此我们没有影响原始值mod 2^n

正确处理整数促销

因为这是一个通用功能,而不仅仅是intand unsigned,所以我们还必须考虑完整的促销规则。有两种可能有趣的情况:一种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

如果short大小相同int(在现代平台上不常见),则积分促销规则会略有不同。在这种情况下,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

在这里,我们看到,铛,GCC和ICC不产生代码cast,并cast_to_signed_integer_basic-O2-O3,和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.2x86_64-linuxg++ -O -S test.cpp

_Z18unsigned_to_signedj:
    movl    %edi, %eax
    ret

UINT_MAX是type的表达式unsigned int,它使您成为整个static_cast<int>(n + INT_MIN) - (UINT_MAX + INT_MIN + 1)类型。不过,应该有可能解决该问题,并且我希望它现在仍可以进行相同的编译。

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;

现在,我们可以安全地投射x2int。让int x3 = static_cast<int>(x2);

我们现在要减去类似UINT_MAX - k * INT_MAX + 1x3如果k > 0

现在,在2s补码系统上,只要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;

请注意,UINT_MAX+1在C ++中保证为零,向int的转换是noop,我们减去了k*INT_MAX后再加到“相同的值”上。因此,可以接受的优化程序应该能够消除所有的假名!

剩下的问题x > INT_MAX还是没有。好吧,我们创建了2个分支,一个带有x > INT_MAX,一个没有。一个不带约束的对象进行强制转换,编译器将其优化为noop。优化程序完成后,带有...的代码将不执行任何操作。智能优化器将两个分支实现为同一事物,并删除该分支。

问题:如果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,因为规范保证每个正号int都可以表示为无符号int。但是UINT_MAX+1在每个系统上都为零;无符号算术总是模UINT_MAX+1。这里仍然可能存在可行的方法……
Nemo 2012年

@Nemo紧跟着这个线程,请原谅我潜在的明显问题:您的陈述“UINT_MAX+1在'03 -spec中建立的每个系统上是否为零?如果是这样,我是否应该查看一个特定的小节?”
WhozCraig

@WhozCraig:第3.9.1节第4节:“声明为无符号的无符号整数,应服从2 ^ n的算术定律,其中n是该特定整数大小的值表示形式中的位数”,并在脚注中说“这意味着无符号算术运算不会溢出,因为不能用所得的无符号整数类型表示的结果的模数要比所得的无符号整数类型可以表示的最大值大一模。基本上,无符号是按照您想要/期望的方式指定的。
Nemo 2012年

@Nemo谢谢。非常感谢。
WhozCraig

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;
}


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


1

我认为int类型至少为两个字节,因此INT_MIN和INT_MAX可能会在不同平台上发生变化。

基本类型

≤climits≥标头


我被诅咒使用默认情况下配置为“ -mint8”的6809编译器,其中int是8位:-((这是Vectrex的开发环境)long是2个字节,long long是4个字节,我不知道短是什么...
Graham Toal

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
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.