C++中的切削位。

7
假设我们有一个包含32位数字的二进制文件。每个32位数字代表一条指令。我的问题是:是否可能直接将这些位切成6+5+5+16的块?类似于:
typedef struct _instruction
{
    int op_code : 6;
    int reg_dest : 5;
    int reg_s1 : 5;
    int offset : 16;
} INST, *PINST;

int read_32_bits = read_next_instr();

INST i = (INST)read_32_bit; /* this would cut the bits into chunks*/

6
可以,但不便携。单词中的位与结构中的位域之间的关系是多种多样的。 - Jerry Coffin
2
C还是C++?选一个。你写的代码表明是前者,而不是后者。 - Lightness Races in Orbit
我更喜欢C++。但是我也可以接受任何一种! - Andro
我给你提供了一个比你选择的更具可移植性且不会引起未定义行为的答案。 - Omnifarious
3个回答

6
为什么不创建一个类似于您所拥有的位结构,然后将其放置在联合内?我删除了typedef,以便使用C++样式定义。
struct instruction
{
    int op_code  : 6;
    int reg_dest : 5;
    int reg_s1   : 5;
    int offset   :16;
};

union INST
{
    instruction  a;
    uint32_t     b;
};

您可以使用网络函数来存储/加载32位值:
INST  i;
i.b = ntohl(value);

现在您可以无需类型转换即可引用位字段。

if (i.a.op_code == XXX)

1
请确保字节序和位顺序正确,这取决于编译器和目标CPU。 - David R Tribble
@bames53 我现在正在查看C规范(ISO/IEC 9899:TC3),但我找不到你所指的内容。第73页(6.5.2.3)的82号脚注将其称为类型转换。这可能是一个陷阱表示。因此,我认为它不被禁止,否则reinterpret_cast也会被禁止了吧? - Jamie Royer
@bames53:是的,我认为你是错误的。事实上,我相信这是改变某些东西的类型而不会引发 UB 的极少数有效方法之一。 - Omnifarious
@JamieRoyer 我认为这个脚注还有更多内容,但我现在找不到了。至于reinterpret_cast,其结果除了在某些特定情况下转换回原始类型之外,不能用于任何其他目的。因此,int i = 1; float f = *reinterpret_cast<float*>(&i); 是未定义行为。我的理解是,要合法地进行这样的转换(具有实现定义的行为),您必须像使用memcpy一样做一些事情。即 int i; float f; memcpy(&i,&f,sizeof(i)); - bames53
@Omnifarious 从一个非活动的联合成员读取数据在C++中肯定是未定义行为。虽然可以使用指向联合成员的指针来进行一些技巧操作,但这样会违反严格别名规则。 - bames53
显示剩余4条评论

4
你可以将你的结构体转换为指向你的32位数据地址的指针。
INST* i = (INST*)&read_32_bit;

然后,您可以像这样访问您的字段:
printf("opcode = %x", i->op_code);

1
+1:有用。确实,这在理论上是“未定义的行为”,而在实践中它不会是可移植的:它将取决于整数是否为32位,它们是否存储为小端或大端整数等等。尽管如此,它很可能会起作用,如果由于字节序问题而不起作用,它将向OP显示为什么在他/她的系统上完全不可能(由于整数中的位没有以与位字段相同的方式布局)。 - ruakh
4
如果可移植性很重要,您可以使用 #ifdef 指令在不同平台上声明多个结构体版本,并根据需要选择正确的位序。 - TJD
1
@BoPersson,这是一个公正的观点。如果你想成为一个纯粹主义者,就不要这样做。但在任何特定的实际平台/编译器上,都会有一个映射可以使用,而且这样做非常实用。 - TJD
2
这实际上是嵌入式系统中的常态。我见过比这更糟糕的事情。+1。 - FailedDev
1
标准的保证只是保证,但仅因为不保证每个符合规范的实现都支持某些内容,并不意味着它在实践中不被支持,或者它不被目标平台支持。原帖作者应该意识到这些限制,但信息不应该被荒谬和无信息价值的“天真神话和谎言”所贬低。 - Jim Balter
显示剩余6条评论

3

以下是一个可行的答案,具有可移植性,在任何编译器上不会调用未定义的行为,并且可以相当有效地进行优化:

struct instruction {
   typedef unsigned int uint_t;

   explicit instruction(uint_t val) : val_(val) {}
   instruction(uint_t op_code, uint_t reg_dest, uint_t reg_s1, uint_t offset)
     : val_((op_code & 0x3fu << 26) | (reg_dest & 0x1fu << 21) |
            (reg_s1 & 0x1fu << 16) | (offset & 0xffffu))
   {
   }

   uint_t op_code() const { return (val_ >> 26) & 0x3fu; }
   void op_code(uint_t newval) { val_ = (newval & 0x3fu << 26) | (val_ & 0x3ffffffu); }

   uint_t reg_dest() const { return (val_ >> 21) & 0x1fu; }
   void reg_dest(uint_t newval) { val_ = (newval & 0x1fu << 21) | (val_ & 0xfc1fffffu); }

   uint_t reg_s1() const { return (val_ >> 16) & 0x1fu; }
   void reg_s1(uint_t newval) { val_ = (newval & 0x1fu) << 16) | (val_ & 0xffe0ffffu); }

   uint_t offset() const { return (val_ >> 16) & 0xffffu; }
   void offset(uint_t newval) const { val_ = (newval & 0xffffu) | (val & 0xffff0000u); }

   uint_t &int_ref() { return val_; }
   uint_t int_ref() const { return val_; }

 private:
   uint_t val_;
};

这样可以使用非常方便的符号访问所有位字段。我认为它也是一个POD,这使得您可以在一些有趣的方式中使用它。如果您连续调用了多个方便函数,则良好的编译器将对位操作进行相当不错的优化。
这几乎和拥有叠加位域一样好。只是在首次定义时需要更多的工作。
此外,我将类型更改为unsigned int,因为如果您在进行位操作,那么您真正想要的是一个简单表示的数字,没有符号位或任何奇怪的东西。理想情况下,您应该包括<cstdint>头文件,并在顶部的typedef中使用::std::uint32_t或类似的东西。

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