如何对浮点数执行位运算

58

我尝试了这个:

float a = 1.4123;
a = a & (1 << 3);

我收到一个编译器错误,说操作数&的类型不能为float。

当我执行以下操作时:

float a = 1.4123;
a = (int)a & (1 << 3);

我成功运行了程序。唯一的问题是按位操作是在舍入后获得的整数表示上进行的。

以下也不被允许。

float a = 1.4123;
a = (void*)a & (1 << 3);

我不明白为什么 int 可以转换为 void*,但 float 却不能。

我这样做是为了解决 Stack Overflow 问题描述中的问题:如何使用遗传算法解决线性方程组?


4
你尝试进行什么样的位运算?你想使用IEEE 754表示特定值吗? - Adam Goode
顺便提一下,a = a & (1<<3) 将清除 a 中除第三位以外的所有位,这通常不是遗传算法中想要的结果。如果要清除单个位,则应使用二进制补码运算符并编写类似 a = a & ~(1<<3) 的代码。 - mob
2
@iamrohitbanga: 方程式???在C++中没有有意义的“方程式”需要对浮点类型执行位运算。 - AnT stands with Russia
2
不会改变任何东西;在C++中也没有需要对浮点数进行位运算的有意义的表达式。 - MSalters
@MSalters,你可以在两个浮点数上使用异或运算符来快速交换它们的值。 - Patrick Roberts
显示剩余5条评论
9个回答

92

在语言级别上,没有所谓的“浮点数按位运算”。C/C++中的按位运算是基于数字的值表示进行的。而浮点数在C/C++中的值表示没有定义(无符号整数是个例外,因为它们的移位被定义为像它们存储在2的补码中一样)。浮点数在值表示层面上没有位,这就是为什么你不能对它们应用按位操作。

你能做的只有分析浮点数所占原始内存的位内容。为此,你需要使用下面建议的联合体或者(等价地,在C++中)将浮点数对象重新解释为一个unsigned char对象数组,例如:

float f = 5;
unsigned char *c = reinterpret_cast<unsigned char *>(&f);
// inspect memory from c[0] to c[sizeof f - 1]

请不要试图像其他答案所建议的那样将float对象重新解释为int对象,这没有多少意义,并且在遵循优化中的严格别名规则的编译器中无法保证正常工作。在C++中检查内存内容的正确方法是将其重新解释为[signed/unsigned] char数组。

此外,请注意,您在系统上的浮点表示并不能保证是IEEE754(尽管在实践中,除非您明确允许它不是,在这种情况下仅涉及-0.0、±infinity和NaN),


17
投票 :) C和C++语言就像数学一样。正式语句的正确性是由硬事实和硬证据定义的,而不是由大多数人的共识决定。大多数人的意见(投票)并不重要。 - AnT stands with Russia
6
@Chap: 你有些困惑了。intchar 的差别是非常大的。在计算机字节中,char 的大小依赖于系统,但在语言层面上,char 的大小是固定的。在语言层面上,char 的大小总是1,这意味着其他每种类型的大小都可以被char的大小整除。此外,unsigned char 中没有填充位,所有的比特位组合都是有效的。而int 并不具备这样的特性。这就是为什么在C++中,每个对象都可以被重新解释为一个char数组,但不能被重新解释为一个[int]数组的原因。 - AnT stands with Russia
3
你所说的有关 float 的系统依赖性表示是正确的,但这恰恰是我的答案要点。正如我所说的,你只能检查 float 对象的原始内存表示形式,这与它是“系统相关”的同义词。要点是,如果 OP 想/需要 由某些原因检查 float 的原始内存表示形式,那么这就是方法。 - AnT stands with Russia
4
在C语言中,做一些"implementation-defined(实现定义)"的事情和做一些"undefined(未定义的)"的事情是有区别的。当我需要做一些"implementation-defined"的事情时,我仍然更喜欢:1)尽可能地将系统依赖降至最低,2)如果可能,避免依赖未定义的行为。这就是为什么使用unsigned char数组比int解决方案更好的原因。 - AnT stands with Russia
3
概念已在 C++ 标准中定义(例如C++03的 3.9/4)。每种对象类型都有对象表示值表示对象表示是对象的原始内存布局,包括值形成位和填充位。值表示仅适用于值形成位,并描述这些位如何编码目标值。 - AnT stands with Russia
显示剩余9条评论

19

如果您想要更改浮点表示中的位,可以尝试以下方法:

union fp_bit_twiddler {
    float f;
    int i;
} q;
q.f = a;
q.i &= (1 << 3);
a = q.f;

正如AndreyT所指出的那样,像这样访问一个union会导致未定义的行为,编译器可能会生长出手臂来扼杀你。请按照他的建议进行操作。


10
从技术上讲,这是未定义行为。你只能访问最后写入的联合成员。 - KeithB
1
在编程中,是否有必要包含一个编译时断言来确保floatint具有相同的大小? - Josh Lee
@KeithB:真的吗,这取决于编译器。标准不是说,位表示应该是相同的吗? - Rohit Banga
@mobrule 我应该改变我的答案吗? - Rohit Banga
@KeithB,我认为在C99中使用这样的联合是可以的?也许我误解了你的观点。你有描述你的意思的链接吗? - Z boson
显示剩余3条评论

10

通过使用memcpy(),您可以绕过严格别名规则,并对被强制转换为uint32_t (如果您的实现定义了它,大多数实现都定义了)的float执行位操作,而不会产生未定义的行为:

float a = 1.4123f;
uint32_t b;

std::memcpy(&b, &a, 4);
// perform bitwise operation
b &= 1u << 3;
std::memcpy(&a, &b, 4);

使用C语言时,这应该是推荐的方法。 - Nicholas Kinar
2
在C++20提案中,使用bit_cast进行类型转换是一种更好(或同样正确)的解决方案,但在此之前,我不知道有什么更好的解决方案。除了绕过这个问题并像接受的答案一样使用unsigned char* - Patrick Roberts
经过测试,我可以证明使用memcpy()函数在C和C++中都能很好地工作。这种方法干净、优雅且自解释。虽然C++20提案中的类型转换也是语言中一个有用的补充,但我可以想象在嵌入式系统编程的上下文中,还会长时间使用memcpy()方法。 - Nicholas Kinar
1
很好的答案,但为了清晰起见,我建议使用sizeof(uint32_t)而不是仅仅使用4。 - Raleigh L.

8
float a = 1.4123;
unsigned int* inta = reinterpret_cast<unsigned int*>(&a);
*inta = *inta & (1 << 3);

3
把内容简化一点:reinterpret_cast<int&>(a) &= (1 << 3) 可以翻译为“将变量a转换为int类型并将其第4位设置为0”。 - Aaron
2
为什么不直接使用(int*)(void*)&a呢? - Cecil Has a Name
1
@Cecil有名字了:使用C++转换 - Chap
5
首选使用C++转换(XXX_cast <>),原因有两点:1)它们更易于搜索,2)reinterpret_cast使您清楚地知道自己正在进行一些系统相关的、可能危险的操作。 - KeithB
你应该将这个操作封装在一个专门处理系统特定操作的类中,因为它严重依赖于目标系统。 - Chap
2
解除引用指针 inta 会导致未定义行为(请参见严格别名)。因此,这种方法不起作用。 - user7860670

5
请看下面的内容。受快速反平方根启发:
#include <iostream>
using namespace std;

int main()
{
    float x, td = 2.0;
    int ti = *(int*) &td;
    cout << "Cast int: " << ti << endl;
    ti = ti>>4;
    x = *(float*) &ti;
    cout << "Recast float: " << x << endl;
    return 0; 
}

1
对指针 (int*) &td(float*) &ti 进行解引用会导致未定义的行为(请参见严格别名)。因此,这种方法不起作用。 - user7860670

3

值得一提的是,对于浮点数进行位运算存在实际用途(我最近遇到了这种情况)——在OpenGL实现中编写着色器时,只支持旧版GLSL(1.2及更早版本不支持位运算符),如果将浮点数转换为整数,则会失去精度。

可以使用余数(模)和不等式检查来对浮点数执行位运算。例如:

float A = 0.625; //value to check; ie, 160/256
float mask = 0.25; //bit to check; ie, 1/4
bool result = (mod(A, 2.0 * mask) >= mask); //non-zero if bit 0.25 is on in A

上述假设A在[0..1)之间且只有一个“位”需要检查,但它可以推广到更复杂的情况。
这个思路基于一些信息,该信息来自于is-it-possible-to-implement-bitwise-operators-using-integer-arithmetic 如果甚至没有内置的模函数,那么也可以相当容易地实现。例如:
float mod(float num, float den)
{
    return num - den * floor(num / den);
}

2

@mobrule:

更好的做法:
#include <stdint.h>
...
union fp_bit_twiddler {
    float f;
    uint32_t u;
} q;

/* mutatis mutandis ... */

对于这些值,使用int类型可能是可以的,但通常情况下,为了避免算术位移的影响,应该使用无符号整型进行位移操作。即使在整数不是32位的系统上,uint32_t也能正常工作。


3
当然,对于浮点数位数不为32位的系统来说,这种方法仍然无法奏效。 - AnT stands with Russia
2
浮点数现在通常遵循IEEE标准,因此浮点数通常为32位,双精度浮点数通常为64位。虽然可能会有例外情况,但我还没有遇到过。不过,assert(sizeof(float)==sizeof(uint32_t)); 很容易编写。 - David Thornley
1
在将浮点数成员赋值后访问联合体的整数成员会导致未定义行为,因此这种方法不起作用。 - user7860670

1

位运算符不应该用于浮点数,因为浮点数是硬件特定的,无论您使用什么硬件,它们都有可能相似。您想要拿哪个项目或工作来冒险“它在我的机器上可行”?相反,在C++中,您可以通过在包装浮点数的“对象”上重载流运算符,获得类似于位移运算符的“感觉”:

// Simple object wrapper for float type as templates want classes.
class Float
{
float m_f;
public:
    Float( const float & f )
    : m_f( f )
    {
    }

    operator float() const
    {
        return m_f;
    }
};

float operator>>( const Float & left, int right )
{
    float temp = left;
    for( right; right > 0; --right )
    {
        temp /= 2.0f;
    }
    return temp;
}

float operator<<( const Float & left, int right )
{
    float temp = left;
    for( right; right > 0; --right )
    {
        temp *= 2.0f;
    }
    return temp;
}

int main( int argc, char ** argv )
{
    int a1 = 40 >> 2; 
    int a2 = 40 << 2;
    int a3 = 13 >> 2;
    int a4 = 256 >> 2;
    int a5 = 255 >> 2;

    float f1 = Float( 40.0f ) >> 2; 
    float f2 = Float( 40.0f ) << 2;
    float f3 = Float( 13.0f ) >> 2;
    float f4 = Float( 256.0f ) >> 2;
    float f5 = Float( 255.0f ) >> 2;
}

你会得到一个余数,根据你所需的实现方式可以将其丢弃。


1
我不确定所有编译器是否会将除法转换为乘以0.5f。出于性能原因,最好这样编写代码,以确保您在不需要浮点数除法时永远不会得到它。 - Peter Cordes

1

Python浮点位运算实现中的Python实现通过将数字表示为二进制来进行浮点位运算,该二进制从小数点左侧和右侧无限延伸。由于大多数架构上的浮点数具有带符号的零,因此它使用补码来表示负数(实际上它只是假装这样做,并使用一些技巧来实现外观)。

我相信它可以适应C++,但必须注意不要让右移溢出,同时平衡指数。


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