请看Oliver Charlesworth的解释,了解为什么这个方法几乎可行。我在回应评论中提出的问题。
由于有几个人指出这个方法不可移植,下面是一些方法可以使其更具可移植性,或者至少让编译器告诉你它是否可行。
首先,C++允许您在编译时检查std::numeric_limits<float>::is_iec559
,例如在static_assert
中。您还可以检查sizeof(int) == sizeof(float)
,如果int
是64位,则不成立,但您真正想要做的是使用uint32_t
,如果存在,则始终恰好为32位宽,具有明确定义的移位和溢出行为,并且如果您的奇怪架构没有这样的整数类型,将导致编译错误。无论哪种方式,您还应该static_assert()
类型具有相同的大小。静态断言没有运行时成本,如果可能的话,您应该始终以这种方式检查前提条件。
很遗憾,将float中的位转换为uint32_t并进行移位的测试是大端、小端还是都不是无法计算为编译时常量表达式。在此,我将运行时检查放在依赖于它的代码部分,但您可能想将其放在初始化中并执行一次。在实践中,gcc和clang都可以在编译时优化掉这个测试。
您不希望使用不安全的指针转换,在我所工作的一些实际系统中,这可能会导致总线错误使程序崩溃。将对象表示转换为最大可移植的方法是使用memcpy()。在我的下面的示例中,我使用union进行类型转换,这适用于任何实际存在的实现。(语言律师反对它,但没有成功的编译器会
默默地破坏那么多遗留代码。)如果必须进行指针转换(请参见下文),则有alignas()。但是,无论如何做,结果都将是实现定义的,这就是我们检查转换和移位测试值的结果的原因。
无论如何,您可能不太可能在现代CPU上使用它,这里是一个经过整理的C++14版本,检查那些非可移植的假设。
#include <cassert>
#include <cmath>
#include <cstdint>
#include <cstdlib>
#include <iomanip>
#include <iostream>
#include <limits>
#include <vector>
using std::cout;
using std::endl;
using std::size_t;
using std::sqrt;
using std::uint32_t;
template <typename T, typename U>
inline T reinterpret(const U x)
{
static_assert( sizeof(T)==sizeof(U), "" );
union tu_pun {
U u = U();
T t;
};
const tu_pun pun{x};
return pun.t;
}
constexpr float source = -0.1F;
constexpr uint32_t target = 0x5ee66666UL;
const uint32_t after_rshift = reinterpret<uint32_t,float>(source) >> 1U;
const bool is_little_endian = after_rshift == target;
float est_sqrt(const float x)
{
static_assert( std::numeric_limits<float>::is_iec559, "" );
assert(is_little_endian);
if ( std::isless(x, 0.0F) || !std::isfinite(x) )
return std::numeric_limits<float>::signaling_NaN();
constexpr uint32_t magic_number = 0x1fbb4000UL;
const uint32_t raw_bits = reinterpret<uint32_t,float>(x);
const uint32_t rejiggered_bits = (raw_bits >> 1U) + magic_number;
return reinterpret<float,uint32_t>(rejiggered_bits);
}
int main(void)
{
static const std::vector<float> test_values{
4.0F, 0.01F, 0.0F, 5e20F, 5e-20F, 1.262738e-38F };
for ( const float& x : test_values ) {
const double gold_standard = sqrt((double)x);
const double estimate = est_sqrt(x);
const double error = estimate - gold_standard;
cout << "The error for (" << estimate << " - " << gold_standard << ") is "
<< error;
if ( gold_standard != 0.0 && std::isfinite(gold_standard) ) {
const double error_pct = error/gold_standard * 100.0;
cout << " (" << error_pct << "%).";
} else
cout << '.';
cout << endl;
}
return EXIT_SUCCESS;
}
更新
这里有一个避免类型转换的reinterpret<T,U>()
的替代定义。您还可以在现代C中实现类型转换,并将函数调用为extern "C"
。我认为类型转换比memcpy()
更加优雅,类型安全并且与该程序的准函数式风格一致。此外,如果存在陷阱表示,则仍然可能具有未定义行为,因此我认为没有太多收益。另外,clang++ 3.9.1 -O -S能够静态分析类型转换版本,将变量is_little_endian
优化为常数0x1
并消除运行时测试,但它只能将此版本优化为单指令存根。
但更重要的是,这段代码不能保证在每个编译器上都能正常工作。例如,一些旧计算机甚至无法精确地寻址32位内存。但在这些情况下,它应该编译失败并告诉您原因。没有编译器会无缘无故地破坏大量遗留代码。虽然标准技术上允许这样做并仍然声称符合C++14,但只会发生在我们预期的架构非常不同的情况下。如果我们的假设是如此无效,以至于某个编译器将float和32位无符号整数之间的类型转换变成危险的错误,那么我真的怀疑这段代码背后的逻辑是否能够通过使用memcpy()来保持稳定。我们希望这段代码在编译时失败,并告诉我们原因。
#include <cassert>
#include <cstdint>
#include <cstring>
using std::memcpy;
using std::uint32_t;
template <typename T, typename U> inline T reinterpret(const U &x)
{
static_assert( sizeof(T)==sizeof(U), "" );
T temp;
memcpy( &temp, &x, sizeof(T) );
return temp;
}
constexpr float source = -0.1F;
constexpr uint32_t target = 0x5ee66666UL;
const uint32_t after_rshift = reinterpret<uint32_t,float>(source) >> 1U;
extern const bool is_little_endian = after_rshift == target;
然而,在
C++核心指南中,Stroustrup等人建议使用
reinterpret_cast
。
#include <cassert>
template <typename T, typename U> inline T reinterpret(const U x)
{
static_assert( sizeof(T)==sizeof(U), "" );
const U temp alignas(T) alignas(U) = x;
return *reinterpret_cast<const T*>(&temp);
}
我测试过的编译器也可以将此代码优化为一个折叠常量。Stroustrup的推理是:
从声明类型不同的对象中访问 reinterpret_cast
的结果仍然是未定义行为,但至少我们可以看到一些棘手的东西正在发生。
更新
根据评论:C++20 引入了 std::bit_cast
,它将对象表示转换为具有未指定而不是未定义的行为的不同类型。这并不保证您的实现将使用与此代码期望的 float
和 int
相同的格式,但它不会给编译器任意打破您的程序的机会,因为在其中一行中存在技术上未定义的行为。它还可以为您提供一个 constexpr
转换。
0x5f3759df
的一种朋友。 - Eugene Sh.f
的位表示向右移动一位大致相当于将指数除以二,这等同于取平方根。其他的部分可能是通过魔法来提高尾数范围内的精度。 - Oliver Charlesworth