高效的无符号签名转换避免了实现定义的行为
我想定义一个函数,将一个unsigned int
作为参数,并返回一个int
全等模UINT_MAX + 1的参数。
第一次尝试可能是这样的:
int unsigned_to_signed(unsigned n) { return static_cast<int>(n); }
但正如任何语言律师所知道的,从无符号到大于INT_MAX的值被赋值是由实现定义的。
(a)只依赖规范规定的行为; 和(b)在任何现代机器上编译成优化编译器。
至于怪异的机器…如果没有签名整型全等模UINT_MAX + 1到无符号整型,让我们说我想抛出一个exception。 如果有不止一个(我不确定这是可能的),假设我想要最大的一个。
好的,第二次尝试:
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年无所不在的符号级系统的瓶颈,那么我敢打赌,有人可以弄清楚,然后优化它。
现在,这第二次尝试是非常接近我想要的。 尽pipe对于某些input来说,强制转换为int
是实现定义的,但是由标准保证强制转换为unsigned
,以保留模数UINT_MAX + 1。 所以有条件的确会检查我想要的东西,而且在我可能遇到的任何系统上它都不会编译成任何东西。
但是…我仍然没有首先检查是否会调用实现定义的行为铸造为int
。 在2050年的某个假设系统中,它可以做谁知道什么。 所以我们说我想避免这种情况。
问题:我的“第三次尝试”应该是什么样子?
回顾一下,我想:
- 从unsigned int转换为signed int
- 保留值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
是true - 将unsigned
n
为int保留0 <= n <= 32767的值,否则返回零
在这个假设的实现中,每个unsigned
值只有一个int
值一致(mod UINT_MAX + 1)。 所以我的问题将是明确的。
我声称这个假设的C ++实现完全符合C ++ 98,C ++ 03和C ++ 11规范。 我承认我并没有记住所有这些词,但我相信我已经仔细阅读了相关章节。 所以如果你想让我接受你的答案,你必须(a)引用一个规范来排除这个假设的实现,或者(b)正确地处理它。
事实上,正确的答案必须处理标准允许的每一个假设的实施。 这就是“只调用标准授权行为”的意思。
顺便提一句,注意std::numeric_limits<int>::is_modulo
在这里完全没有用处,原因有很多。 首先,即使未签名转换签名转换不适用于较大的无符号值,情况也是如此。 另一方面,如果算术仅仅是整数整数范围,那么即使在补码或符号幅度系统上也是如此。 等等。 如果你的答案取决于is_modulo
,那就错了。
[更新2]
hvd的答案教会了我一些东西:我的假设C ++实现整数是现代C所不允许的。C99和C11标准对于有符号整数的表示非常具体; 事实上,它们只允许二补,补码和符号的大小(第6.2.6.2段(2);)。
但C ++不是C.事实certificate,这个事实在我的问题的核心。
原来的C ++ 98标准是基于C89的旧版本,它说(第3.1.2.5节):
对于每个有符号整数types,都有一个对应的(但是不同的)无符号整数types(用关键字unsigned来指定)使用相同的存储量(包括符号信息)并且具有相同的alignment要求。 有符号整数types的非负值的范围是相应的无符号整数types的子范围,并且每种types中相同值的表示是相同的。
C89没有提到只有一个符号位或者只允许二进制补码/一个补码/符号量级。
C ++ 98标准几乎逐字采用了这种语言(第3.9.1段(3)):
对于每个有符号整数types,都存在一个对应的(但不同的) 无符号整数types :“
unsigned char
”,“unsigned short int
”,“unsigned int
”和“unsigned long int
”,每个types占用相同的数量并具有与相应的带符号整数types相同的alignment要求(3.9); 也就是说,每个有符号整数types与其对应的无符号整数types具有相同的对象表示。 有符号整数types的非负值的范围是相应的无符号整数types的子范围,每个相应的有符号/无符号types的值表示应该是相同的。
C ++ 03标准使用基本相同的语言,就像C ++ 11一样。
据我所知,没有标准的C ++规范将其有符号整数表示限制在任何C规范中。 并没有任何要求一个单一的标志位或任何types的东西。 它所说的是, 非负的有符号整数必须是相应的无符号的子范围。
所以,我再次声明INT_MAX = 32767与INT_MIN = -2 32 +32768是允许的。 如果你的答案是否定的,那么这是不正确的,除非你引用一个C ++标准certificate我错了。
扩大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
至less等于-INT_MIN – 1的math值。
在最常见的系统中,其中!(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的整数types(6.2.6.2)表示形成的,它需要n
位为值位,一位为符号位,并且只允许一个陷阱表示(不包括由于填充位而无效的表示)即否则将表示负零/ -INT_MAX - 1
。 C ++不允许超出C允许的任何整数表示。
更新 :微软的编译器显然没有注意到x > 10
和x >= 11
testing了同样的事情。 如果x >= INT_MIN
被replace为x > INT_MIN - 1u
,它可以检测为x <= INT_MAX
(在此平台上)的否定,它只会生成所需的代码。
[提问者(尼莫)的更新,详细阐述我们在下面的讨论]
我现在认为这个答案在所有情况下都适用,但是由于复杂的原因。 我很可能会奖励这个解决scheme,但是我想捕获所有关心的细节。
我们从C ++ 11的第18.3.3节开始:
表31描述了标题
<climits>
。…
内容与标准C库头
<limits.h>
。
在这里,“标准C”是指C99,它的规范严格限制了有符号整数的表示。 它们就像无符号整数一样,但有一位专用于“符号”,零位或多位专用于“填充”。 填充位对整数的值没有贡献,符号位仅作为二进制补码,一个补码或符号幅度来提供。
由于C ++ 11从C99inheritance了<climits>
macros,因此INT_MIN是-INT_MAX或-INT_MAX-1,并且hvd的代码可以保证工作。 (请注意,由于填充,INT_MAX可能远远小于UINT_MAX / 2 …但是感谢signed-> unsigned casts的工作方式,这个答案处理罚款。)
C + + 03 / C + + 98更棘手。 它使用相同的措词从“标准C”inheritance<climits>
,但是现在“标准C”意味着C89 / C90。
所有这些–C ++ 98,C ++ 03,C89 / C90 – 都有我在我的问题给出的措辞,还包括这个(C ++ 03第3.9.1节第7段):
(44)[ 例子 :本国际标准允许二进制补码,1的补码和有符号整数types的幅度表示。]
脚注(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
表示一个非连续的值范围,而spec规则中的其他措辞则表示出来。
最后,如何提供一个小负值的符号位? 我们可以有一个1的“符号位”的贡献,比如-37的int值吗? 那么INT_MAX是(比如说)2 31 -1,INT_MIN是-37?
这将导致一些数字有两个表示…但是一个补码给两个表示为零,这是允许根据“示例”。 规范中没有任何地方说零是唯一可能有两个表示的整数。 所以我认为这个新的假设是规范允许的。
事实上,从-1到-1的任何负值似乎都是允许的,作为“符号位”的值,但不能小一些(以免范围是不连续的)。 换句话说, INT_MIN
可能是从-INT_MAX-1
到-1的任何东西。
现在猜猜怎么样? 对于第二次强制转换的代码,以避免实现定义的行为,我们只需要x - (unsigned)INT_MIN
小于或等于INT_MAX
。 我们刚刚显示INT_MIN
至less是-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 ++ 03inheritance。 它在C99中是固定的,并且C ++ 11通过并入来自C99的<limits.h>
来间接地inheritance该修补程序。 但即使是C ++ 11也保留了自相矛盾的“纯二进制expression”措辞。
这个代码只依赖于规范要求的行为,所以要求(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)和铛3.0(-Os,-O,-O2,-O3)的no-op。 英特尔12.1.0拒绝优化这个。 而我没有关于Visual C的信息
你可以明确地告诉编译器你想要做什么:
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
如果x
是我们的input…
如果x > INT_MAX
,我们希望find一个常数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
东西。
现在,在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;
请注意,保证C ++中UINT_MAX+1
为零,转换为int是一个noop,并且我们减去k*INT_MAX
然后将其添加回“相同的值”。 所以一个可以接受的优化器应该能够清除所有这些错误!
这就导致了x > INT_MAX
的问题。 那么我们创build2个分支,一个是x > INT_MAX
,另一个是没有的。 没有一个做海峡演习,编译器优化为noop。 在…之后,优化器完成后,会执行一个noop操作。 智能优化器将两个分支实现为同一个事物,并删除分支。
问题:如果UINT_MAX
相对于INT_MAX
确实很大,则上述可能不起作用。 我假设k*INT_MAX <= UINT_MAX+1
隐含。
我们可以用一些枚举来攻击它:
enum { divisor = UINT_MAX/INT_MAX, remainder = UINT_MAX-divisor*INT_MAX };
我认为(我们保证这个math运算能够工作吗?这很棘手…),并基于这些逻辑来轻松优化非二进制补码系统。
这也打开了例外情况。 只有在UINT_MAX比(INT_MIN-INT_MAX)大得多的情况下才可以,所以你可以把你的exception代码放在一个if块中,以某种方式准确地询问这个问题,而不会在传统的系统上减慢你的速度。
我不确定如何构build这些编译时常量来正确处理。
std::numeric_limits<int>::is_modulo
是一个编译时间常量。 所以你可以使用它来模板专业化。 问题解决了,至less如果编译器和内联一起玩的话。
#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的古代configuration版本)可能的陷阱。 为了简单起见,这是通过在这样的机器上(即,在Clearpath上)不支持值-2 n -1来完成的,其中n是整数值位的数目。 在实践中,这个值将不被机器支持(即,具有符号和量值或1的补码表示)。
我认为inttypes至less有两个字节,所以INT_MIN和INT_MAX可能会在不同的平台上发生变化。
基本types
≤climits≥头
我的钱在使用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,苹果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
这是完全符合标准的,并将在MSVC / gcc上编译为无操作。
int unsigned_to_signed(unsigned int n) { union UltimateCast { unsigned int In; int Out; } cast; cast.In = n; return cast.Out; }
对于调用代码如:
volatile unsigned int i = 32167; int main() { return unsigned_to_signed( i ); }
我们将有这个程序集输出(g ++ -O3 -S):
__Z18unsigned_to_signedj: movl 4(%esp), %eax ret _main: pushl %ebp movl %esp, %ebp andl $-16, %esp call ___main movl _i, %eax leave ret .globl _i .data .align 4 _i: .long 32167
并将unsigned_to_signed()
声明为inline
:
_main: pushl %ebp movl %esp, %ebp andl $-16, %esp call ___main movl _i, %eax leave ret .globl _i .data .align 4 _i: .long 32167
这是相当整洁的代码。