utl::bit
utl::bit module provides clean implementations for some of the common bit operations. Main highlights are:
- Individual bit manipulation
- Group bit operations
- Works with arbitrary sized integers
- Proper handling of signed types
- Type-safe enum bitflags
[!Note] Significant part of this module gets added into the standard library with C++20
<bits>
, all such functions provide similar API to allow for a seamless transition. The only difference is thatstd
requires inputs to be unsigned, whilebit
works on both signed & unsigned.
Definitions
// Individual bit operations
template <class T> constexpr bool get(T value, std::size_t bit) noexcept;
template <class T> constexpr T set(T value, std::size_t bit) noexcept;
template <class T> constexpr T clear(T value, std::size_t bit) noexcept;
template <class T> constexpr T flip(T value, std::size_t bit) noexcept;
// Group bit operations
template <class T> constexpr T lshift(T value, std::size_t shift) noexcept;
template <class T> constexpr T rshift(T value, std::size_t shift) noexcept;
template <class T> constexpr T rotl(T value, std::size_t shift) noexcept;
template <class T> constexpr T rotr(T value, std::size_t shift) noexcept;
// Utils
constexpr std::size_t byte_size = CHAR_BIT;
template <class T> constexpr std::size_t size_of;
template <class T> constexpr std::size_t width(T value) noexcept;
// Enum Bitflags
template<class E>
struct Flags {
constexpr Flags( E flag) noexcept;
constexpr Flags(std::initializer_list<E> flag_list) noexcept;
constexpr operator bool() const noexcept;
constexpr E get() const noexcept;
constexpr bool contains(E flag) const noexcept;
constexpr bool contains(Flags other) const noexcept;
constexpr Flags& add(E flag) noexcept;
constexpr Flags& add(Flags other) noexcept;
constexpr Flags& remove(E flag) noexcept;
constexpr Flags& remove(Flags other) noexcept;
// + bit-wise operators
// + comparison operators
};
Methods
Individual bit operations
template <class T> constexpr bool get(T value, std::size_t bit) noexcept;
Returns the state of the bit
in an integer value
.
Note: This and all consequent functions assume bit < sizeof(T) * CHAR_BIT
, otherwise behavior is undefined. This precondition is checked in DEBUG
.
template <class T> constexpr T set(T value, std::size_t bit) noexcept; template <class T> constexpr T clear(T value, std::size_t bit) noexcept; template <class T> constexpr T flip(T value, std::size_t bit) noexcept;
Sets / clears / flips the bit
in an integer value
and returns the result.
Group bit operations
template <class T> constexpr T lshift(T value, std::size_t shift) noexcept; template <class T> constexpr T rshift(T value, std::size_t shift) noexcept;
Shifts bits in an integer value
left / right by a given shift
, shifted from bits are zero-filled.
Functionally identical to operators <<
and >>
, but with a proper handling for negative signed integers. In the standard shifting negative numbers is considered UB, in practice every single compiler implements signed bit-shift as (signed)((unsigned)value << shift)
while implicitly assuming value > 0
since negatives are UB. This implicit assumption can lead to incorrectly generated code since branches with value < 0
will be eliminated as “dead code” simply through the virtue of being near a value << shift
statement. These functions perform the cast explicitly leading to the exact same performance & behavior but safer.
Note: This and all consequent functions assume shift < sizeof(T) * CHAR_BIT
, otherwise behavior is undefined. This precondition is checked in DEBUG
.
template <class T> constexpr T rotl(T value, std::size_t shift) noexcept; template <class T> constexpr T rotr(T value, std::size_t shift) noexcept;
Shifts bits in an integer value
left / right by a given shift
, shifted from bits wrap around.
This operation usually compiles down to a single instruction and is often used in computational code.
Utils
constexpr std::size_t byte_size = CHAR_BIT;
Convenience constant, exists purely to reduce the usage of macros. Evaluates to 8
on most sane platforms.
template <class T> constexpr std::size_t size_of;
Convenience constant. Evaluates to the size of T
in bits, which equals sizeof(T) * byte_size
.
template <class T> constexpr std::size_t width(T value) noexcept;
Returns the number of significant bits in an integer.
Note: Unsigned integers have ceil(log2(value))
significant bits.
Enum Bitflags
constexpr Flags::Flags( E flag) noexcept; constexpr Flags::Flags(std::initializer_list<E> flag_list) noexcept;
Constructs bitflag object from one or several enum values.
constexpr operator bool() const noexcept;
Converts to false
if underlying bitflag value is 0
, otherwise true
.
constexpr E get() const noexcept;
Returns the underlying enum class
value.
constexpr bool Flags::contains(E flag) const noexcept; constexpr bool Flags::contains(Flags other) const noexcept;
Returns whether bitflag object contains a specific flag(s).
constexpr Flags& add(E flag) noexcept; constexpr Flags& add(Flags other) noexcept; constexpr Flags& remove(E flag) noexcept; constexpr Flags& remove(Flags other) noexcept;
Adds / removes flag(s) from a bitflag object.
Several adds / removes can be chained in a single statement.
constexpr Flags operator~() const noexcept; constexpr Flags operator|(Flags other) const noexcept; constexpr Flags operator&(Flags other) const noexcept; constexpr Flags& operator|=(Flags other) noexcept; constexpr Flags& operator&=(Flags other) noexcept;
Bitwise operators used for classic bitflag semantics.
constexpr bool operator==(Flags other) noexcept; constexpr bool operator!=(Flags other) noexcept; constexpr bool operator<=(Flags other) noexcept; constexpr bool operator>=(Flags other) noexcept; constexpr bool operator< (Flags other) noexcept; constexpr bool operator> (Flags other) noexcept;
Comparison operators, effectively same as comparing the underlying value.
Examples
Working with individual bits
[ Run this code ]
using namespace utl;
constexpr std::uint8_t x = 19; // 19 ~ 00010011
// human-readable notation is big-endian, which means bits are indexed right-to-left
// Read bits
static_assert(bit::get(x, 0) == 1);
static_assert(bit::get(x, 1) == 1);
static_assert(bit::get(x, 2) == 0);
static_assert(bit::get(x, 3) == 0);
static_assert(bit::get(x, 4) == 1);
static_assert(bit::get(x, 5) == 0);
static_assert(bit::get(x, 6) == 0);
static_assert(bit::get(x, 7) == 0);
// Modify bits
static_assert(bit::set( x, 2) == 23); // 23 ~ 00010111
static_assert(bit::clear(x, 0) == 18); // 18 ~ 00010010
static_assert(bit::flip( x, 1) == 17); // 17 ~ 00010001
General usage
[ Run this code ]
using namespace utl;
constexpr std::uint8_t x = 19; // 19 ~ 00010011
// Group bit operations
static_assert(bit::rotl( x, 6) == 196); // 196 ~ 11000100
static_assert(bit::rotr( x, 1) == 137); // 137 ~ 10001001
static_assert(bit::lshift(x, 6) == 192); // 192 ~ 11000000
static_assert(bit::rshift(x, 1) == 9); // 9 ~ 00001001
// Utils
static_assert(bit::width(x) == 5); // 00010011 has 5 significant bits
static_assert(bit::size_of<std::uint16_t> == 16);
static_assert(bit::size_of<std::uint32_t> == 32);
static_assert(bit::size_of<std::uint64_t> == 64);
Using enum bitflags
[ Run this code ]
using namespace utl;
// Bitflag-suitable enum class
enum class IOMode { IN = 1 << 0, OUT = 1 << 1, APP = 1 << 2 };
// Function taking enum flags
void open_file(bit::Flags<IOMode> flags) {
if (flags.contains(IOMode::IN) ) log::println(" > File opened for reading" );
if (flags.contains(IOMode::OUT)) log::println(" > File opened for writing" );
if (flags.contains(IOMode::APP)) log::println(" > File opened for appending");
}
// ...
log::println("Opening file with OUT:");
open_file(IOMode::OUT);
log::println("Opening file with OUT | APP:");
open_file(bit::Flags{IOMode::OUT, IOMode::APP});
Output:
Opening file with OUT:
> File opened for writing
Opening file with OUT | APP:
> File opened for writing
> File opened for appending
Additional bitflag examples
[ Run this code ]
using namespace utl;
// Bitflag-suitable enum class
enum class IOMode { IN = 1 << 0, OUT = 1 << 1, APP = 1 << 2 };
// Simple yet flexible API, same thing can be accomplished
// both with classic bit-wise semantics and with built-in methods.
// Underneath it's just a strongly typed integer so there is no performance impact
constexpr auto flags_1 = bit::Flags{IOMode::OUT, IOMode::APP};
constexpr auto flags_2 = bit::Flags(IOMode::OUT) | bit::Flags(IOMode::APP);
constexpr auto flags_3 = bit::Flags(IOMode::OUT) | IOMode::APP;
constexpr auto flags_4 = bit::Flags(IOMode::OUT).add(IOMode::APP);
constexpr auto flags_5 = bit::Flags<IOMode>{}.add(IOMode::OUT).add(IOMode::APP);
static_assert(flags_1 == flags_2 && flags_2 == flags_3 && flags_3 == flags_4 && flags_4 == flags_5);