浮点数位和严格别名

23

我试图从浮点数中提取位,而不会引发未定义的行为。这是我的第一次尝试:

unsigned foo(float x)
{
    unsigned* u = (unsigned*)&x;
    return *u;
}

据我理解,由于严格的别名规则,这并不保证能够正常工作,对吗?如果我使用一个字符指针进行中间步骤,它是否有效?
unsigned bar(float x)
{
    char* c = (char*)&x;
    unsigned* u = (unsigned*)c;
    return *u;
}

还是需要我自己提取每个字节吗?
unsigned baz(float x)
{
    unsigned char* c = (unsigned char*)&x;
    return c[0] | c[1] << 8 | c[2] << 16 | c[3] << 24;
}

当然,这种方法有依赖于大小端的缺点,但我可以接受。

使用联合体的技巧肯定是未定义行为,对吗?

unsigned uni(float x)
{
    union { float f; unsigned u; };
    f = x;
    return u;
}

为了完整起见,这里提供了 foo 的参考版本。也是未定义行为,对吧?

unsigned ref(float x)
{
    return (unsigned&)x;
}

那么,从一个浮点数中提取位是否可能(当然,假设两者都是32位宽)?编辑:这里是Goz提出的memcpy版本。由于许多编译器尚不支持static_assert,因此我用一些模板元编程替换了static_assert
template <bool, typename T>
struct requirement;

template <typename T>
struct requirement<true, T>
{
    typedef T type;
};

unsigned bits(float x)
{
    requirement<sizeof(unsigned)==sizeof(float), unsigned>::type u;
    memcpy(&u, &x, sizeof u);
    return u;
}

1
@Ebomike:第一种方法违反了严格别名规则。请阅读这篇文章:http://cellperformance.beyond3d.com/articles/2006/06/understanding-strict-aliasing.html。 - Goz
谢谢,我知道总有人会证明我是错的 :) - EboMike
1
@Johannes:未定义行为怎么是最安全的选择呢?:)写入一个联合成员,然后从另一个成员读取是未定义的。 - fredoverflow
1
@FredOverflow 嗯,即使是UB,我也不认为编译器会费尽心思地起诉你。无论如何,请参见下面的版本,它没有这个问题。GCC的激进优化已经在其手册中记录下来,允许您进行联合转换。允许必要的恶行(有时不希望使用库函数或依赖于编译器内部函数来优化特定的memcpy使用)。 - Johannes Schaub - litb
@Aprogrammer:你是指“联合体技巧”吧?结构体技巧与未知大小数组作为结构体的最后一个成员有关。 - fredoverflow
显示剩余2条评论
4个回答

17

要真正避免任何问题,唯一的方法就是使用memcpy。

unsigned int FloatToInt( float f )
{
   static_assert( sizeof( float ) == sizeof( unsigned int ), "Sizes must match" );
   unsigned int ret;
   memcpy( &ret, &f, sizeof( float ) );
   return ret;
}

由于你正在复制一个固定的数量,编译器将对其进行优化。

尽管如此,联合方法被广泛支持。


@FredOverflow ... 打错了 ;) 已经修正。 - Goz
@Crashworks:你可以放心地报告一个错误……但这并不意味着编译器的作者会在意;他们的编译器仍然可能完全符合标准。 - Goz
1
合规的,且不是我们购买的! - Crashworks
2
@Crashworks,呵呵呵。个人而言,我使用memcpy技巧。这让别人非常清楚你正在做什么 :) - Goz
2
根据POSIX(http://pubs.opengroup.org/onlinepubs/9699919799/functions/memcpy.html)和ISO C标准,它是void *。数据在内部的解释留给实现。例如,gcc将memcpys转换为循环,每次传输一个基本机器单元,然后使用较短的加载/存储传输剩余部分。 - onitake
显示剩余2条评论

6

联合体的使用是未定义行为,对吗?

是和不是。根据标准,它确实是未定义行为。但这是一个常用的技巧,因此GCC、MSVC以及我所知道的其他流行编译器都明确保证它是安全的,并且可以按预期工作。


除了你将浮点数误解为整数之外,它的哪个部分是未定义行为? - EboMike
4
只是不被允许。一个工会只有一个成员处于“活动”状态。如果你写入结构体的一个成员,那么你只能从同一成员中读取。读取任何其他成员的结果是未定义的。 - jalf
2
@EboMike 除此之外,UB 就是这样的。如果从一个与联合体活动成员不兼容的成员中读取,则会违反别名规则。例如,以下代码是正确的:union A { int a; unsigned char b; }; A x = { 10 }; return x.b;,因为你可以通过 unsigned char 类型的左值访问 int - Johannes Schaub - litb
目前规范中没有禁止 union A { int a; float b; }; A x = { 0 }; float *b = &x.b; *b = 0.f; return x.b; 的概念。在这种情况下,通过浮点指针写入将活动成员切换为 float,但当该写操作发生在单独的函数中时,这就成为了一个问题(编译器基本上无法按照标准所预期的别名规则进行应用)。请参见 http://www.open-std.org/jtc1/sc22/wg21/docs/cwg_active.html#636。 - Johannes Schaub - litb
@JohannesSchaub-litb:看起来简单的常识答案应该是说,取一个联合成员的地址应该允许通过所得指针或由它派生的指针使用对象,直到代码下一次访问其他联合成员(通过不是从前述成员派生的指针),跨越发生这种情况的循环的开始,或进入发生这种情况的函数。这应该是简单而实用的实现,不会影响许多实际有用的优化,同时处理联合成员指针的常见用例。 - supercat

5
以下内容不违反别名规则,因为它没有在任何地方使用访问不同类型的lvalues。
template<typename B, typename A>
B noalias_cast(A a) { 
  union N { 
    A a; 
    B b; 
    N(A a):a(a) { }
  };
  return N(a).b;
}

unsigned bar(float x) {
  return noalias_cast<unsigned>(x);
}

这证明了标准是有问题的。temporary.member 不是一个左值,这太荒谬了。我猜 std 的人被“rvalue”(作为值)和“rvalue”(临时值)这些术语搞混了。哈哈 - curiousguy
2
@Johannes:这个推理仍然正确吗?访问 b 是访问联合体的非活动成员。 - GManNickG

0
如果你真的想对浮点类型的大小保持不可知,并且只返回原始位,可以像这样做:
void float_to_bytes(char *buffer, float f) {
    union {
        float x;
        char b[sizeof(float)];
    };

    x = f;
    memcpy(buffer, b, sizeof(float));
}

然后这样调用:

float a = 12345.6789;
char buffer[sizeof(float)];

float_to_bytes(buffer, a);

这种技术当然会产生与您机器的字节顺序特定的输出。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接