我正在寻找一种相对高效的方法来确定一个浮点数(double
)是否可以被整数数据类型(long
, 64位)准确地表示。
我的初步想法是检查指数是否为0
(或更确切地说是127
)。但这不会奏效,因为2.0
的e=1 m=1 ...
所以基本上,我陷入了困境。我有一种感觉,可以用位掩码来实现,但目前还没有头绪。
那么,如何检查一个double能否被准确地表示为long呢?
谢谢
我想我已经找到了一种以符合标准的方式将一个 double
转换为整数的方法(这不是问题的真正问题,但这可以很大程度上帮助解决问题)。首先,我们需要看看为什么显而易见的代码是错误的。
// INCORRECT CODE
uint64_t double_to_uint64 (double x)
{
if (x < 0.0) {
return 0;
}
if (x > UINT64_MAX) {
return UINT64_MAX;
}
return x;
}
问题在于第二个比较中,UINT64_MAX
被隐式转换为double
。C标准没有精确规定这种转换的工作方式,只规定它要四舍五入到可表示的值。这意味着,即使第二个比较在数学上应该是真的(当UINT64_MAX
四舍五入,而'x'在UINT64_MAX
和(double)UINT64_MAX
之间时可能发生),它也可能是假的。因此,在这种边缘情况下,将double
转换为uint64_t
可能会导致未定义的行为。UINT64_MAX
在double
中可能无法完全表示,但是UINT64_MAX+1
作为二的幂(并且不太大),肯定是可以表示的。因此,如果我们先将输入舍入为整数,则比较x > UINT64_MAX
等同于x >= UINT64_MAX+1
,除了可能在加法中溢出。我们可以使用ldexp
而不是将一个加到UINT64_MAX
来修复溢出。也就是说,以下代码应该是正确的。/* Input: a double 'x', which must not be NaN.
* Output: If 'x' is lesser than zero, then zero;
* otherwise, if 'x' is greater than UINT64_MAX, then UINT64_MAX;
* otherwise, 'x', rounded down to an integer.
*/
uint64_t double_to_uint64 (double x)
{
assert(!isnan(x));
double y = floor(x);
if (y < 0.0) {
return 0;
}
if (y >= ldexp(1.0, 64)) {
return UINT64_MAX;
}
return y;
}
现在回到你的问题:在 uint64_t
中,x
是否能够被精确表示?只有在它既没有舍入也没有夹紧的情况下才可以。
/* Input: a double 'x', which must not be NaN.
* Output: If 'x' is exactly representable in an uint64_t,
* then 1, otherwise 0.
*/
int double_representable_in_uint64 (double x)
{
assert(!isnan(x));
return (floor(x) == x && x >= 0.0 && x < ldexp(1.0, 64));
}
同样的算法可以用于不同大小的整数,也可以通过微小修改用于带符号整数。以下代码对 uint32_t
和 uint64_t
版本进行了一些非常基本的测试(只能可能捕获到假阳性),但也适合手动检查边缘情况。
#include <inttypes.h>
#include <math.h>
#include <limits.h>
#include <assert.h>
#include <stdio.h>
uint32_t double_to_uint32 (double x)
{
assert(!isnan(x));
double y = floor(x);
if (y < 0.0) {
return 0;
}
if (y >= ldexp(1.0, 32)) {
return UINT32_MAX;
}
return y;
}
uint64_t double_to_uint64 (double x)
{
assert(!isnan(x));
double y = floor(x);
if (y < 0.0) {
return 0;
}
if (y >= ldexp(1.0, 64)) {
return UINT64_MAX;
}
return y;
}
int double_representable_in_uint32 (double x)
{
assert(!isnan(x));
return (floor(x) == x && x >= 0.0 && x < ldexp(1.0, 32));
}
int double_representable_in_uint64 (double x)
{
assert(!isnan(x));
return (floor(x) == x && x >= 0.0 && x < ldexp(1.0, 64));
}
int main ()
{
{
printf("Testing 32-bit\n");
for (double x = 4294967295.999990; x < 4294967296.000017; x = nextafter(x, INFINITY)) {
uint32_t y = double_to_uint32(x);
int representable = double_representable_in_uint32(x);
printf("%f -> %" PRIu32 " representable=%d\n", x, y, representable);
assert(!representable || (double)(uint32_t)x == x);
}
}
{
printf("Testing 64-bit\n");
double x = ldexp(1.0, 64) - 40000.0;
for (double x = 18446744073709510656.0; x < 18446744073709629440.0; x = nextafter(x, INFINITY)) {
uint64_t y = double_to_uint64(x);
int representable = double_representable_in_uint64(x);
printf("%f -> %" PRIu64 " representable=%d\n", x, y, representable);
assert(!representable || (double)(uint64_t)x == x);
}
}
}
以下方法在大多数情况下都可以使用。如果您输入 NaN
、INF
或非常大的(溢出)数字,我不确定它是否会失效,也不确定如何失效...
(尽管我认为它们都将返回 false - 无法准确表示。)
您可以:
类似这样:
double val = ... ; // Value
if ((double)(long long)val == val){
// Exactly representable
}
floor()
和ceil()
同样适用(尽管如果值超过整数范围可能会失败):
floor(val) == val
ceil(val) == val
这里是一个混乱的位掩码解决方案:
该解决方案使用联合类型转换,假设IEEE双精度。 联合类型转换仅在C99 TR2及更高版本中有效。
int representable(double x){
// Handle corner cases:
if (x == 0)
return 1;
// -2^63 is representable as a signed 64-bit integer, but +2^63 is not.
if (x == -9223372036854775808.)
return 1;
// Warning: Union type-punning is only valid in C99 TR2 or later.
union{
double f;
uint64_t i;
} val;
val.f = x;
uint64_t exp = val.i & 0x7ff0000000000000ull;
uint64_t man = val.i & 0x000fffffffffffffull;
man |= 0x0010000000000000ull; // Implicit leading 1-bit.
int shift = (exp >> 52) - 1075;
// Out of range
if (shift < -52 || shift > 10)
return 0;
// Test mantissa
if (shift < 0){
shift = -shift;
return ((man >> shift) << shift) == man;
}else{
return ((man << shift) >> shift) == man;
}
}
ieee754.h
中包含的标准ieee754_float
联合来获取尾数和指数,而要使用非标准联合转换呢? - ircmaxellieee754.h
是由哪个标准规定的? - R.. GitHub STOP HELPING ICE您可以使用modf函数将浮点数拆分为整数和小数部分。modf函数在标准C库中。
#include <math.h>
#include <limits.h>
double val = ...
double i;
long l;
/* check if fractional part is 0 */
if (modf(val, &i) == 0.0) {
/* val is an integer. check if it can be stored in a long */
if (val >= LONG_MIN && val <= LONG_MAX) {
/* can be exactly represented by a long */
l = val;
}
}
(double)(long)x == x
,但要避免直接使用它。当x
超出范围时,(long)x
是未定义行为(UB)。
(long)x
的有效转换范围为LONG_MIN - 1 < x < LONG_MAX + 1
,因为在转换过程中代码会丢弃x
的任何小数部分。因此,代码需要使用FP math来测试x
是否在范围内。#include <limits.h>
#include <stdbool.h>
#define DBL_LONG_MAXP1 (2.0*(LONG_MAX/2+1))
#define DBL_LONG_MINM1 (2.0*(LONG_MIN/2-1))
bool double_to_long_exact_possible(double x) {
if (x < DBL_LONG_MAXP1) {
double whole_number_part;
if (frexp(x, &whole_number_part) != 0.0) {
return false; // Fractional part exist.
}
#if -LONG_MAX == LONG_MIN
// rare non-2's complement machine
return x > DBL_LONG_MINM1;
#else
return x - LONG_MIN > -1.0;
#endif
}
return false; // Too large or NaN
}
任何IEEE浮点数的double
或float
值,其大小在2^52或2^23及以上,将成为整数。将2^52或2^23加到其大小小于该值的正数上将导致其四舍五入为整数。减去添加的值将产生一个整数,如果原始值是整数,则等于原始值。请注意,此算法对于某些大于2^52的数字将失败,但对于那么大的数字不需要使用此算法。
你能使用模运算符来检查双精度数是否可以被1整除吗?还是我完全误解了这个问题?
double val = ... ; // Value
if(val % 1 == 0) {
// Val is evenly divisible by 1 and is therefore a whole number
}
double
类型没有operator%
运算符,对吧? - Seth Carnegie
DBL_MANT_DIG
,表示double
的尾数中数字的数量。标准中“数字”的单位是FLT_RADIX
;对于“普通”的IEEE754double
,基数为2,尾数有53个这样的“数字”(也称为...位)。因此,在完全精度下最具代表性的整数应为(FLT_RADIX << (DBL_MANT_DIG-1)) - 1
。 - FrankH.