Benjamin James Drury
Benjamin James Drury

Reputation: 2383

Getting entire value from bitfields

I wish to create a Block struct for use in a voxel game I am building (just background context), however I have run into issues with my saving and loading.

I can either represent a block as a single Uint16 and shift the bits to get the different elements such as blockID and health, or I can use a bitfield such as the one below:

struct Block
{
    Uint16 id : 8;
    Uint16 health : 6;
    Uint16 visible : 1;
    Uint16 structural : 1;
}

With the first method, when I wish to save the Block data I can simply convert the value of the Uint16 into a hex value and write it to a file. With loading I can simply read and convert the number back, then go back to reading the individual bits with manual bit shifting.

My issue is that I cannot figure out how to get the whole value of the Uint16 I am using with the bitfields method, which means I cannot save the block data as a single hex value.

So, the question is how do I go about getting the actual single Uint16 stored inside my block struct that is made up from the different bit fields. If it is not possible then that is fine, as I have already stated my manual bit shifting approach works just fine. I just wanted to profile to see which method of storing and modifying data is faster really.

If I have missed out a key detail or there is any extra information you need to help me out here, by all means do ask.

Upvotes: 2

Views: 2518

Answers (5)

A union is probably the cleanest way:

#include <iostream>

typedef unsigned short Uint16;

struct S {
  Uint16 id : 8;
  Uint16 health : 6;
  Uint16 visible : 1;
  Uint16 structural : 1;
};
union U {
 Uint16 asInt;
 S asStruct;
};

int main() {
  U u;
  u.asStruct.id = 0xAB;
  u.asStruct.health = 0xF;
  u.asStruct.visible = 1;
  u.asStruct.structural = 1;
  std::cout << std::hex << u.asInt << std::endl;
}

This prints out cfab.

Update:

After further consideration and reading more deeply about this I have decided that any kind of type punning is bad. Instead I would recommend just biting the bullet and explicitly do the bit-twiddling to construct your value for serialization:

#include <iostream>

typedef unsigned short Uint16;

struct Block
{
  Uint16 id : 8;
  Uint16 health : 6;
  Uint16 visible : 1;
  Uint16 structural : 1;

  operator Uint16() {
    return structural | visible << 2 | health << 4 | id << 8;
  }
};

int main() {
  Block b{0xAB, 0xF, 1, 1};
  std::cout << std::hex << Uint16(b) << std::endl;
}

This has the further bonus that it prints abf5 which matches the initializer order.

If you are worried about performance, instead of using the operator member function you could have a function that the compiler optimizes away:

...

constexpr Uint16 serialize(const Block& b) {
  return b.structural | b.visible << 2 | b.health << 4 | b.id << 8;
}

int main() {
  Block b{0xAB, 0xF, 1, 1};
  std::cout << std::hex << serialize(b) << std::endl;
}

And finally if speed is more important than memory, I would recommend getting rid of the bit fields:

struct Block
{
  Uint16 id;
  Uint16 health;
  Uint16 visible;
  Uint16 structural;
};

Upvotes: 1

Filip Ros&#233;en
Filip Ros&#233;en

Reputation: 63862

Living on the edge (of undefined-behavior)..

The naive solution would be to reinterpret_cast a reference to the object to the underlying type of your bit-field, abusing the fact that the first non-static data-member of a standard-layout class is located at the same address as the object itself.

struct A {         
  uint16_t         id : 8;
  uint16_t     health : 6;
  uint16_t    visible : 1;
  uint16_t structural : 1;
};

A a { 0, 0, 0, 1 };
uint16_t x = reinterpret_cast<uint16_t const&> (a);

The above might look accurate, and it will often (not always) yield the expected result - but it suffers from two big problems:

  • The allocation of bit-fields within an object is implementation-defined, and;
  • the class type must be standard-layout.


There is nothing saying that the bit-fields will, physically, be stored in the order you declare them, and even if that was the case a compiler might insert padding between every bit-field (as this is allowed).

To sum things up; how bit-fields end up in memory is highly implementation-defined, trying to reason about the behavior requires you to look into your implementations documentation on the matter.


What about using a union?


Recommendation

Stick with the bit-fiddling approach, unless you can absolutely prove that every implementation on which the code is ran handles it the way you would want it to.


What does the standard (N4296) say?

9.6p1 Bit-fields [class.bit]

[...] Allocation of bit-fields within a class object is implementation-defined. Alignment of bit-fields is implementation-defined. [...]

9.2p20 Classes [class]

If a standard-layout class object has any non-static data members, its address is the same as the address of its first non-static data member. [...]

Upvotes: 1

Mehmet Fide
Mehmet Fide

Reputation: 1794

You can use union:

typedef union
{
    struct
    {
        Uint16 id : 8;
        Uint16 health : 6;
        Uint16 visible : 1;
        Uint16 structural : 1;
    } Bits;

    Uint16 Val;
} TMyStruct;

Upvotes: 0

Thomas Matthews
Thomas Matthews

Reputation: 57729

There are at least two methods for what you want:

  • Bit Shifting
  • Casting

Bit Shifting

You can build a uint16_t from your structure by shifting the bit fields into a uint16_t:

uint16_t halfword;
struct Bit_Fields my_struct;
halfword = my_struct.id << 8;
halfword = halfword | (my_struct.health << 2);
halfword = halfword | (my_struct.visible << 1);
halfword = halfword | (my_struct.structural);

Casting

Another method is to cast the instance of the structure to a uint16_t:

uint16_t halfword;
struct Bit_Fields my_struct;
halfword = (uint16_t) my_struct;

Endianess

One issue of concern is Endianness; or the byte ordering of multi-byte values. This may play a part with where the bits lie within the 16-bit unit.

Upvotes: 1

Collin Dauphinee
Collin Dauphinee

Reputation: 13993

This isn't a good usage of bit fields (and really, there are very few).

There is no guarantee that the order of your bit fields will be the same as the order they're declared in; it could change between builds of your application.

You'll have to manually store your members in a uint16_t using the shift and bitwise-or operators. As a general rule, you should never just dump or blindly copy data when dealing with external storage; you should manually serialize/deserialize it, to ensure it's in the format you expect.

Upvotes: 0

Related Questions