这里有几篇有关浮点数及其性质的文章。显然,comparing floats and doubles必须始终谨慎进行。 Asking for equality也已经讨论过,建议显然不要使用它。
但是如果直接分配该怎么办:
double a = 5.4;
double b = a;
假设a
是任何非NaN的值-a == b
可以为假吗?
似乎答案显然不是,但是我找不到在C ++环境中定义此行为的任何标准。 IEEE-754声明两个具有相同(非NaN)位模式的浮点数相等。现在是否意味着我可以继续以这种方式比较我的双打而不必担心可维护性?我是否需要担心其他编译器/操作系统及其在这些方面的实现?还是编译器优化了一些位并破坏了它们的相等性?
我编写了一个小程序,该程序永久生成并比较非NaN随机双精度数-直到发现a == b
产生false
的情况。我是否可以在将来随时随地编译/运行此代码,而不必期望暂停? (忽略字节序,并假定符号,指数和尾数位大小/位置保持不变)。
#include <iostream>
#include <random>
struct double_content {
std::uint64_t mantissa : 52;
std::uint64_t exponent : 11;
std::uint64_t sign : 1;
};
static_assert(sizeof(double) == sizeof(double_content), "must be equal");
void set_double(double& n, std::uint64_t sign, std::uint64_t exponent, std::uint64_t mantissa) {
double_content convert;
memcpy(&convert, &n, sizeof(double));
convert.sign = sign;
convert.exponent = exponent;
convert.mantissa = mantissa;
memcpy(&n, &convert, sizeof(double_content));
}
void print_double(double& n) {
double_content convert;
memcpy(&convert, &n, sizeof(double));
std::cout << "sign: " << convert.sign << ", exponent: " << convert.exponent << ", mantissa: " << convert.mantissa << " --- " << n << '\n';
}
int main() {
std::random_device rd;
std::mt19937_64 engine(rd());
std::uniform_int_distribution<std::uint64_t> mantissa_distribution(0ull, (1ull << 52) - 1);
std::uniform_int_distribution<std::uint64_t> exponent_distribution(0ull, (1ull << 11) - 1);
std::uniform_int_distribution<std::uint64_t> sign_distribution(0ull, 1ull);
double a = 0.0;
double b = 0.0;
bool found = false;
while (!found){
auto sign = sign_distribution(engine);
auto exponent = exponent_distribution(engine);
auto mantissa = mantissa_distribution(engine);
//re-assign exponent for NaN cases
if (mantissa) {
while (exponent == (1ull << 11) - 1) {
exponent = exponent_distribution(engine);
}
}
//force -0.0 to be 0.0
if (mantissa == 0u && exponent == 0u) {
sign = 0u;
}
set_double(a, sign, exponent, mantissa);
b = a;
//here could be more (unmodifying) code to delay the next comparison
if (b != a) { //not equal!
print_double(a);
print_double(b);
found = true;
}
}
}
使用Visual Studio Community 2017版本15.9.5
答案 0 :(得分:6)
C ++标准在[basic.types]#3中明确规定:
对于任何平凡可复制的类型
T
,如果两个指向T
的指针指向不同的T
对象obj1
和obj2
,则两个{{1} }或obj1
都不是潜在重叠的子对象,如果将构成obj2
的基础字节([intro.memory])复制到obj1
中,则obj2
随后应保存与obj2
相同。
它给出了这个例子:
obj1
剩下的问题是T* t1p;
T* t2p;
// provided that t2p points to an initialized object ...
std::memcpy(t1p, t2p, sizeof(T));
// at this point, every subobject of trivially copyable type in *t1p contains
// the same value as the corresponding subobject in *t2p
是什么。我们发现[basic.fundamental]#12(重点是我):
共有三种浮点类型:
value
,float
和double
。 类型long double
的精度至少与double
相同,而类型float
的精度至少与long double
相同。 类型double
的值集是类型float
的值集的子集;double
类型的值集是double
类型的值集的子集。 浮点类型的值表示形式是实现定义的。
由于C ++标准对浮点值的表示方式没有任何进一步的要求,因此,这是该标准的保证,因为只需要赋值即可保存值({{3 }}):
在简单分配(
long double
)中,通过用右操作数的结果替换其值来修改左操作数所引用的对象。
正如您正确观察到的那样,IEEE-754要求,当且仅当非NaN浮点具有相同的位模式时,它们的比较才相等。因此如果编译器使用符合IEEE-754的浮点数,则应该发现分配非NaN浮点数会保留位模式。
事实上,您的代码
=
不应允许double a = 5.4;
double b = a;
返回false。但是,一旦您用更复杂的表达式替换(a == b)
时,这些优点就消失了。这不是本文的确切主题,但是[expr.ass]#2提到了几种看起来无辜的代码可以产生不同结果的可能方式(这打破了“与位模式相同”的断言)。特别是,您可能正在将80位中间结果与64位舍入结果进行比较,可能会产生不等式。
答案 1 :(得分:3)
这里有些并发症。首先,请注意标题提出的问题与问题不同。标题问:
分配两个双精度数是否保证产生相同的位集模式?
当问题问到时:
a == b永远是假的吗?
第一个问题询问分配是否可能出现不同的位(这可能是由于分配未记录与其右操作数相同的值,还是由于分配使用了表示相同值的不同位模式) ,而第二个询问是否由赋值写入了什么位,存储的值必须比较等于操作数。
从总体上讲,第一个问题的答案为否。使用IEEE-754二进制浮点格式,在非零数值和它们在位模式下的编码之间存在一对一的映射。但是,这允许分配在几种情况下产生不同的位模式:
关于后一个问题,在a == b
和a = b
都为a
的{{1}}之后,b
是否可以为假,答案是否定的。 C ++标准确实要求赋值将左操作数的值替换为右操作数的值。因此,在double
之后,a = b
必须具有a
的值,因此它们是相等的。
请注意,C ++标准并未对浮点运算的准确性施加任何限制(尽管我仅在非规范性说明中看到了这一点)。因此,从理论上讲,人们可能会将浮点值的赋值或比较解释为浮点运算,并说它们不需要准确性,因此赋值可能会更改值或比较会返回不准确的结果。我认为这不是对该标准的合理解释;对浮点精度的限制没有限制,这是为了使表达式评估和库例程具有更大的自由度,而不是简单的赋值或比较。
请注意,以上内容专门适用于从简单的b
操作数分配的double
对象。这不应使读者感到自满。几种类似但不同的情况可能会导致数学上似乎直观的失败,例如:
double
之后,表达式float x = 3.4;
的计算结果通常为false,因为x == 3.4
是3.4
,并且必须将double
转换为float
。分配。这种转换会降低精度并更改值。double x = 3.4 + 1.2;
之后,C ++标准允许表达式x == 3.4 + 1.2
取值为false。这是因为该标准允许以比标称类型所需的精度更高的精度来计算浮点表达式。因此,可以3.4 + 1.2
的精度来评估long double
。将结果分配给x
时,标准要求“丢弃多余的精度”,因此将值转换为double
。与上面的float
示例一样,此转换可能会更改该值。然后,比较x == 3.4 + 1.2
可以将double
中的x
值与long double
产生的3.4 + 1.2
值进行比较。