🚩

Bit Flags

CategoryC++

Overview

Flags are values that can be combined using bitwise-operations.

They are often implemented as enum types.

C

In C, it's common to define flags as preprocessor defines.

#define FileModeRead   = 0x1
#define FileModeWrite  = 0x2
#define FileModeAppend = 0x4

Or as constant integers.

static const unsigned FileModeRead   = 0x1;
static const unsigned FileModeWrite  = 0x2;
static const unsigned FileModeAppend = 0x4;

C++

Enum type

In C++, enums are not scoped, and it's a best practice to add a prefix to each member.

enum PrimitiveType
{
    PrimitiveType_Point,
    PrimitiveType_Line,
    PrimitiveType_LineStrip,
    PrimitiveType_Triangle,
    PrimitiveType_TriangleStrip,
};

PrimitiveType type = PrimitiveType_Triangle;

Enum types have an integer underlying type.

By default, the first constant is set to 0, and the value increases with each constant sequentially.

It's also possible to specify the value for each constant instead.

enum PrimitiveType
{
    PrimitiveType_Point = 0,
    PrimitiveType_Line = 1,
    PrimitiveType_LineStrip = 2,
    PrimitiveType_Triangle = 3,
    PrimitiveType_TriangleStrip = 4,
};

PrimitiveType type = PrimitiveType_Triangle;

Enum class type

Since C++11, enum class has been introduced and provide a scope for enum values.

enum class PrimitiveType
{
    Point,
    Line,
    LineStrip,
    Triangle,
    TriangleStrip,
};

PrimitiveType type = PrimitiveType::Triangle;

Flags with enum

Enum types can be used as flags by specifying power-of-two values.

enum FileMode
{
    FileMode_Read   = 1,
    FileMode_Write  = 2,
    FileMode_Append = 4,
};

The values can be specied sequentially using the bitwise-left operator <<.

enum class FileMode
{
    FileMode_Read   = (1 << 0),
    FileMode_Write  = (1 << 1),
    FileMode_Append = (1 << 2),
};

The values can be combined using a bitwise-or operation.

int mode = (FileMode_Read | FileMode_Write);

The result has to be stored into an int value, but we can do a static_cast to the enum type.

FileMode mode = static_cast<FileMode>(FileMode_Read | FileMode_Write);

We can check if a flag contains a specific value using a bitwise-and operation.

bool canRead = ((mode & FileMode_Read) != 0);
⚠️
One major problem with the enum type is that it is automatically promoted into an integer value and it's easy to create invalid values by combining different enum types together.
int mode = FileMode_Read | PrimitiveType_TriangleStrip;

In this case, mode will have an integer value of 4, which is not the expected result.

When the result is cast to FileMode, it will have an invalid value that can create an undefined behavior.

FileMode mode = static_cast<FileMode>(FileMode_Read | PrimitiveType_TriangleStrip);

Flags with enum class

We can define flags using the enum class type to increase the type safety.

enum class FileMode
{
  Read   = (1 << 0),
  Write  = (1 << 1),
  Append = (1 << 2),
};
⚠️
Unfortunately, bitwise operations does not compile.
FileMode mode = (FileMode::Read | FileMode::Write);
bool canRead = ((mode & FileMode::Read) != 0);
⚠️
To be able to compile we need to perform many static casts.
FileMode mode = (static_cast<FileMode>(static_cast<int>(FileMode::Read) | static_cast<int>(FileMode::Write)));
bool canRead = ((static_cast<int>(mode) & static_cast<int>(FileMode::Read)) != 0);
⚠️
Furthermore, it doesn't solve the mismatch issue as we need to cast to int to perform the bitwise operations.
FileMode mode = static_cast<FileMode>(static_cast<int>(FileMode::Read) | static_cast<int>(PrimitiveType::TriangleStrip));

It can still produce invalid values.

Flags class

We can define custom operators for each bitwise operation.

We use std::underlying_type instead of a hardcoded integer type.

Bitwise operations

Flags operator &(Flags lhs, Flags rhs)  
{
    return static_cast<Flags> (
        static_cast<std::underlying_type<Flags>::type>(lhs) &
        static_cast<std::underlying_type<Flags>::type>(rhs)
    );
}

Flags operator |(Flags lhs, Flags rhs)  
{
    return static_cast<Flags> (
        static_cast<std::underlying_type<Flags>::type>(lhs) |
        static_cast<std::underlying_type<Flags>::type>(rhs)
    );
}

Flags operator ^(Flags lhs, Flags rhs)  
{
    return static_cast<Flags> (
        static_cast<std::underlying_type<Flags>::type>(lhs) ^
        static_cast<std::underlying_type<Flags>::type>(rhs)
    );
}

Flags operator ~(Flags value)  
{
    return static_cast<Flags> (
        ~static_cast<std::underlying_type<Flags>::type>(value)
    );
}

Bitwise assignments

Flags& operator &=(Flags& lhs, Flags rhs)  
{
    lhs = static_cast<Flags> (
        static_cast<std::underlying_type<Flags>::type>(lhs) &
        static_cast<std::underlying_type<Flags>::type>(rhs)
    );

    return lhs;
}

Flags& operator |=(Flags& lhs, Flags rhs)  
{
    lhs = static_cast<Flags> (
        static_cast<std::underlying_type<Flags>::type>(lhs) |
        static_cast<std::underlying_type<Flags>::type>(rhs)
    );

    return lhs;}

Flags& operator ^=(Flags& lhs, Flags rhs)  
{
    lhs = static_cast<Flags> (
        static_cast<std::underlying_type<Flags>::type>(lhs) ^
        static_cast<std::underlying_type<Flags>::type>(rhs)
    );

    return lhs;
}