-
Notifications
You must be signed in to change notification settings - Fork 0
CPP Packed Enums
Value Enums and Flag Enums can be packed more tightly within a struct by utilizing bit fields. Several macros are provided to make this easier:
ENUMBRA_PACK_UNINITIALIZED(Enum, Name)
Declare an enum. The value is NOT initialized, you must do so through your constructor.
ENUMBRA_INIT_DEFAULT(Name)
Use inside a constructor initializer list to initialize the member with the config-specified default value.
ENUMBRA_INIT(Name, InitValue)
Use inside a constructor initializer list to initialize the member with a user-given value.
The type of InitValue is checked at compile-time to make sure it is a valid enumbra type.
C++20 now allows default member initialization for bit-fields, making initialization simpler:
ENUMBRA_PACK_INIT_DEFAULT(Enum, Name)
Declare and initialize an enum with the config-specified default value.
ENUMBRA_PACK_INIT(Enum, Name, InitValue)
Declare and initialize an enum with a given value.
The type of InitValue is checked at compile-time to make sure it is a valid enumbra type.
// Using a flags enum that looks like this:
enum class EDirectionFlags : uint8_t { North = 1, East = 2, South = 4, West = 8 }
struct Packed
{
ENUMBRA_PACK_UNINITIALIZED(EDirectionFlags, Player1Directions);
ENUMBRA_PACK_UNINITIALIZED(EDirectionFlags, Player2Directions);
ENUMBRA_PACK_UNINITIALIZED(EDirectionFlags, Player3Directions);
ENUMBRA_PACK_UNINITIALIZED(EDirectionFlags, Player4Directions);
// Constructor is required to initialize values!
Packed() :
// Initialize with config-defined default value
ENUMBRA_INIT_DEFAULT(Player1Directions),
// Initialize with a fixed value
ENUMBRA_INIT(Player2Directions, EDirectionFlags::South),
// This works too
ENUMBRA_INIT(Player3Directions, EDirectionFlags::South | EDirectionFlags::West),
// NOT RECOMMENDED! This will zero-initialize the value, but zero may not be a
// valid value for that enum! Prefer ENUMBRA_INIT_DEFAULT instead.
Player4Directions()
{ }
};
static_assert(sizeof(Packed) == 2); // each enum requires 4 bits and the underlying type is uint8_t
// Using a flags enum that looks like this:
enum class EDirectionFlags : uint8_t { North = 1, East = 2, South = 4, West = 8 }
// Bit field member initializers are now supported on C++20 compilers:
struct PackedInlineInit
{
ENUMBRA_PACK_INIT(EDirectionFlags, Player1, EDirectionFlags::West);
ENUMBRA_PACK_INIT(EDirectionFlags, Player2, EDirectionFlags::North | EDirectionFlags::South);
ENUMBRA_PACK_INIT_DEFAULT(EDirectionFlags, Player3);
ENUMBRA_PACK_INIT_DEFAULT(EDirectionFlags, Player4);
// Constructor not required
};
All of the general rules of C++ bit fields still apply:
- Their layout is implementation defined and non-portable, so do not transfer them over the network or serialize without a conversion method. It is implementation defined if bit fields may straddle type boundaries or introduce padding.
- The underlying type of an enum determines the minimum storage, padding, and alignment.
- Adjacent bit fields with differing underlying types may or may not share storage.
- Bit fields are more compact in memory but require extra instructions to read/modify/write. Benchmark your use-case to determine if they are the right choice.
- enumbra specific:
- The
|=
,&=
, and^=
operator overloads are not provided for the underlying enum class type, since doing so is not possible with bit fields (because bit fields cannot be passed as non-const references).
- The
Raymond Chen has some good advice on the topic: https://devblogs.microsoft.com/oldnewthing/20081126-00/?p=20073
Due to the way that signed integers map their ranges to bit fields, a bit field may require an additional bit of storage to accommodate the sign bit even if it is unused. For example, given the following enum:
// How many bits do we need to store these values?
enum class EValueBits { A = 0, B = 1, C = 2, D = 3 }
int8_t Value : 1; // maps to the range -1 - 0, unexpected! Two's complement is fun!
int8_t Value : 2; // maps to the range -2 - 1, Still not big enough
int8_t Value : 3; // maps to the range -4 - 3, Wasting space
uint8_t Value : 2; // maps to the range 0 - 3, best utilization of space
On top of this, signed bitfields have yet another disadvantage in that they must insert extra instructions to maintain the value of the sign bit when packing and unpacking.