Search code examples
c++bit-manipulationhandlebit-fields

Concise bit-manipulation for 64bit integer handle type


I have a 64bit integer that is used as a handle. The 64bits must be sliced into the following fields, to be accessed individually:

size           : 30 bits
offset         : 30 bits
invalid flag   : 1 bit
immutable flag : 1 bit
type flag      : 1 bit
mapped flag    : 1 bit

The two ways I can think of to achieve this are:

1) Traditional bit operations (& | << >>), etc. But I find this a bit cryptic.

2) Use a bitfield struct:

#pragma pack(push, 1)
struct Handle {
    uint32_t size      : 30;
    uint32_t offset    : 30;
    uint8_t  invalid   : 1;
    uint8_t  immutable : 1;
    uint8_t  type      : 1;
    uint8_t  mapped    : 1;
};
#pragma pack(pop)

Then accessing a field becomes very clear:

handle.invalid = 1;

But I understand bitfields are quite problematic and non-portable.

I'm looking for ways to implement this bit manipulation with the object of maximizing code clarity and readability. Which approach should I take?

Side notes:

  • The handle size must not exceed 64bits;

  • The order these fields are laid in memory is irrelevant, as long as each field size is respected;

  • The handles are not saved/loaded to file, so I don't have to worry about endianess.


Solution

  • I would go for the bitfields solution.

    Bitfields are only "non-portable" if you want to store the in binary form and later read the bitfield using a different compiler or, more commonly, on a different machine architecture. This is mainly because field order is not defined by the standard.

    Using bitfields within your application will be fine, and as long as you have no requirement for "binary portability" (storing your Handle in a file and reading it on a different system with code compiled by a different compiler or different processor type), it will work just fine.

    Obviously, you need to do some checking, e.g. sizeof(Handle) == 8 should be done somewhere, to ensure that you get the size right, and compiler hasn't decided to put your two 30-bit values in separate 32-bit words. To improve the chances of success on multiple architectures, I'd probably define the type as:

    struct Handle {
        uint64_t size      : 30;
        uint64_t offset    : 30;
        uint64_t invalid   : 1;
        uint64_t immutable : 1;
        uint64_t type      : 1;
        uint64_t mapped    : 1;
    };
    

    There is some rule that the compiler should not "split elements", and if you define something as uint32_t, and there are only two bits left in the field, the whole 30 bits move to the next 32-bit element. [It probably works in most compilers, but just in case, using the same 64-bit type throughout is a better choice]