| Document #: | P4313R0 [Latest] [Status] |
| Date: | 2026-07-14 |
| Project: | Programming Language C++ |
| Audience: |
Library Evolution |
| Reply-to: |
Iliya Guterman <iliyagutermann@gmail.com> Anthony Williams <anthony@justsoftwaresolutions.co.uk> |
We propose enabling bitmask operators for enum classes that have a
std::bitmask_type
annotation, to provide similar behavior to 16.3.3.3.3
[bitmask.types].
enum class [[=std::bitmask_type]] Permission {
None = 0,
Read = 1 << 0,
Write = 1 << 1,
Execute = 1 << 2,
};
int main () {
// Combine permissions
Permission user_perms = Permission::Read | Permission::Write;
if (user_perms & Permission::Read == Permission::Read)
std::cout << "Can read";
}As a developer coming from languages other than C++, I grew accustomed to using enums to represent flags. However, such behavior is noticeably absent from the C++ language.
from enum import IntFlag
class Permission(IntFlag):
READ = 1 << 0 # 0b001
WRITE = 1 << 1 # 0b010
EXECUTE = 1 << 2 # 0b100
# Combine permissions
user_perms = Permission.READ | Permission.WRITE
# Check permissions
if user_perms & Permission.READ:
print("Can read")using System;
[Flags]
public enum Permission
{
None = 0,
Read = 1 << 0, // 0b001
Write = 1 << 1, // 0b010
Execute = 1 << 2, // 0b100
}
class Program
{
static void Main()
{
// Combine permissions
Permission userPerms = Permission.Read | Permission.Write;
Console.WriteLine(userPerms);
// Read, Write
// Check permissions
if ((userPerms & Permission.Read) != 0) {
Console.WriteLine("Can read");
}
}
}// Cargo.toml: bitflags = "2.4" (or latest)
use bitflags::bitflags;
bitflags! {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Permissions: u32 {
const READ = 0b00000001;
const WRITE = 0b00000010;
const EXECUTE = 0b00000100;
}
}
fn main() {
// Combine flags using bitwise OR
let user_perms = Permissions::READ | Permissions::WRITE;
// Check permissions
if user_perms.contains(Permissions::READ) {
println!("Can read");
}
}const Permission = {
READ: 1 << 0, // 0b001
WRITE: 1 << 1, // 0b010
EXECUTE: 1 << 2, // 0b100
};
// Combine permissions
let userPerms = Permission.READ | Permission.WRITE;
// Check permissions
if (userPerms & Permission.READ) {
console.log("Can read");
}A repeated boilerplate code for bitmask behavior can be found with quite high frequency in the wild
For example, in LLVM project, the same boilerplate code is repeated multiple times. - perms.h - byte.h - perm_options.h
enum class perms : unsigned {
none = 0,
owner_read = 0400,
owner_write = 0200,
owner_exec = 0100,
owner_all = 0700,
...
}
[[nodiscard]] _LIBCPP_HIDE_FROM_ABI inline constexpr perms operator&(perms __lhs, perms __rhs) {
return static_cast<perms>(static_cast<unsigned>(__lhs) & static_cast<unsigned>(__rhs));
}
[[nodiscard]] _LIBCPP_HIDE_FROM_ABI inline constexpr perms operator|(perms __lhs, perms __rhs) {
return static_cast<perms>(static_cast<unsigned>(__lhs) | static_cast<unsigned>(__rhs));
}
[[nodiscard]] _LIBCPP_HIDE_FROM_ABI inline constexpr perms operator^(perms __lhs, perms __rhs) {
return static_cast<perms>(static_cast<unsigned>(__lhs) ^ static_cast<unsigned>(__rhs));
}
[[nodiscard]] _LIBCPP_HIDE_FROM_ABI inline constexpr perms operator~(perms __lhs) {
return static_cast<perms>(~static_cast<unsigned>(__lhs));
}
_LIBCPP_HIDE_FROM_ABI inline perms& operator&=(perms& __lhs, perms __rhs) { return __lhs = __lhs & __rhs; }
_LIBCPP_HIDE_FROM_ABI inline perms& operator|=(perms& __lhs, perms __rhs) { return __lhs = __lhs | __rhs; }
_LIBCPP_HIDE_FROM_ABI inline perms& operator^=(perms& __lhs, perms __rhs) { return __lhs = __lhs ^ __rhs; }
_LIBCPP_END_NAMESPACE_FILESYSTEMThe above can be replaced with the following:
enum class [[=std::bitmask_type]] perms : unsigned {
none = 0,
owner_read = 0400,
owner_write = 0200,
owner_exec = 0100,
owner_all = 0700,
...
};This type of use case is a sought-after feature and has led to the development of many standalone solutions.
enum class
typedef enum
instead?Reverting to using C-style enums means losing the benefits of scoped
constants and the type safety provided by C++
enum class.
typedef enum Permission {
PERMISSION_NONE = 0,
PERMISSION_READ = 1u << 0, // 0b001
PERMISSION_WRITE = 1u << 1, // 0b010
PERMISSION_EXECUTE = 1u << 2, // 0b100
} Permission;
int main(void)
{
Permission user_perms = PERMISSION_READ | PERMISSION_WRITE;
// Check permissions
if (user_perms & PERMISSION_READ) {
printf("Can read\n");
}
}While std::bitset supports bitwise operations, it is limited in other respects.
std::bitset requires the user to provide meaningful names, for example:
#include <bit>
#include <bitset>
#include <cassert>
#include <iostream>
#include <stdexcept>
class BitSetA : public std::bitset<8> {
public:
static constexpr std::size_t write_flag = 2;
// Users must create meaningful function names or additional abstractions to represent the individual bit names
bool is_write_enabled() {
return test(write_flag);
}
void toggle_write_enabled() {
flip(write_flag);
}
};
int main () {
BitSetA a;
std::cout << a.is_write_enabled() << std::endl;
a.toggle_write_enabled();
std::cout << a.is_write_enabled() << std::endl;
}std::bitset
also does not provide the same type-safety guarantees as
enum class.
The resulting type remains the generic
std::bitset
type, so the original semantic type information is lost during
operations.
See for example this StackOverflow question: C++ enum flags vs bitset
#include <bit>
#include <bitset>
#include <cassert>
#include <iostream>
#include <stdexcept>
class BitSetA : public std::bitset<8> { };
class BitSetB : public std::bitset<8> { }
int main () {
BitSetA a;
BitSetB b;
// a and b decay into std::bitset and lose their type safety
std::bitset<8> c = a & b;
}The design builds upon the solution proposed by Andreas Fertig C++20 Concepts applied - Safe bitmasks using scoped enums which in itself is an extension to Using Enum Classes as Bitfields initially devised by Anthony Williams
intenum class
is easy and simple for the user.#include <type_traits>
#include <utility>
#include <meta>
namespace std {
inline constexpr struct BitmaskType {} bitmask_type;
template <typename T>
concept BitmaskTypeLike =
std::is_enum_v<T> and (!std::meta::annotations_of_with_type(^^T,std::meta::type_of(^^bitmask_type)).empty());
} // std
template <std::BitmaskTypeLike T>
constexpr T operator|(T lhs, T rhs) {
return static_cast<T>(std::to_underlying(lhs) | std::to_underlying(rhs));
}
template <std::BitmaskTypeLike T>
constexpr T operator&(T lhs, T rhs) {
return static_cast<T>(std::to_underlying(lhs) & std::to_underlying(rhs));
}
template <std::BitmaskTypeLike T>
constexpr T operator^(T lhs, T rhs) {
return static_cast<T>(std::to_underlying(lhs) ^ std::to_underlying(rhs));
}
template <std::BitmaskTypeLike T> constexpr T operator~(T lhs) {
return static_cast<T>(~std::to_underlying(lhs));
}
template <std::BitmaskTypeLike T>
constexpr T& operator|=(T &lhs, T rhs) {
lhs = lhs | rhs;
return lhs;
}
template <std::BitmaskTypeLike T>
constexpr T& operator&=(T &lhs, T rhs) {
lhs = lhs & rhs;
return lhs;
}
template <std::BitmaskTypeLike T> constexpr T& operator^=(T &lhs, T rhs) {
lhs = lhs ^ rhs;
return lhs;
}<bitmask>
headerAdd new header
<bitmask>
to table Table 24: C++ library 16.4.2.3
[headers]
With the content:
22.x.1 Header
<bitmask>synopsisThe header
<bitmask>defines all function needed for 16.3.3.3.3 [bitmask.types]namespace std { // 22.x.2, annotation type inline constexpr unspecified bitmask_type = unspecified; // 22.x.3, concept template <typename T> concept bitmask-like = see below; // exposition only } // 22.x.4, goes into global namespace template <bitmask-like T> constexpr T operator|(T lhs, T rhs); template <bitmask-like T> constexpr T operator&(T lhs, T rhs); template <bitmask-like T> constexpr T operator^(T lhs, T rhs); template <bitmask-like T> constexpr T operator~(T lhs); template <bitmask-like T> constexpr T& operator|=(T &lhs, T rhs); template <bitmask-like T> constexpr T& operator&=(T &lhs, T rhs); template <bitmask-like T> constexpr T& operator^=(T &lhs, T rhs);22.x.2 Annotation
bitmask_typenamespace std { inline constexpr unspecified bitmask_type; }1 bitmask_type is an instance of a unique unspecified type that is suitable for use as an annotation (9.13.12 [dcl.attr.annotation]).
22.x.3 Concept bitmask-like
namespace std { template <typename T> concept bitmask-like = std::is_enum_v<T> and (!std::meta::annotations_of_with_type(^^T,std::meta::type_of(^^bitmask_type)).empty()); }[ Note: bitwise operators matching those required for Bitmask types [bitmask.types] are defined for types that conform to the bitmask-like concept. — end note ]
[ Example:enum class [[=std::bitmask_type]] Permission { None = 0, read = 1 << 0, Write = 1 << 1, Execute = 1 << 2, };The enum
Permissionconforms to the concept bitmask-like — end example ]22.x.4 bitmask operators
1 For each operator @ in (|, ^, &) there is a definition in the global namespace of the form:
template<bitmask-like T> constexpr T operator@ (T lhs,T rhs) noexcept { return static_cast<T>(to_underlying(lhs) @ to_underlying(rhs)); }2 And the corresponding compound assignment operators have definitions in the global namespace of the form:
template<bitmask-like T> constexpr T& operator@= (T &lhs,T rhs) noexcept { lhs = lhs @ rhs; return lhs; }3 Additionally the complement operator is defined in the global namespace as:
template<bitmask-like T> constexpr T operator~ (T lhs) noexcept { return static_cast<T>(~to_underlying(lhs)); }