Skip to content

Instantly share code, notes, and snippets.

@berkakinci
Last active December 27, 2025 19:34
Show Gist options
  • Select an option

  • Save berkakinci/7d610f7f479a2a1a3249860dd5771176 to your computer and use it in GitHub Desktop.

Select an option

Save berkakinci/7d610f7f479a2a1a3249860dd5771176 to your computer and use it in GitHub Desktop.
Bit manipulation is easy in Verilog HDL. It is still tedious in C++, but the language is evolving for the better.

Compile-time Bit Masks for Hardware-Adjacent C++

This code is a small building block extracted from a larger register / field interaction framework in some driver code I recently wrote for Invitrometrix.

The focus here is not on templates or language features for their own sake, but on clean, intention-revealing usage when interacting with hardware registers.

I come from the hardware side, where bits, fields, and widths are explicit and easily accessed. This code is an attempt to make register-level C++ feel a little friendlier to that world.


Usage Benefits

The primary goal is to make register manipulation read closer to how you think about the underlying hardware.

Instead of repeating bitwise operations and masks at every call site, that information is defined exactly once and used expressively:

auto mymask = Mask<uint32_t>::of_bits(6, 7);

// clear masked field, then insert field from new_value
value = mymask.merge(new_value, value);

For comparison, the traditional approach typically looks something like this:

#define MASK 0x1F80
value = (new_value & MASK) | (value & ~MASK);

The problem with the traditional form is not correctness — it’s cognitive load. Every use site requires thinking about bitwise operations and type-related hazards. Some use sites must re-derive and re-validate the same knowledge about bit widths and offsets. Once masks become named, validated objects instead of raw numbers, usage becomes mechanical and much harder to get wrong.

Mask Generation

Under the hood, masks are generated using a small, reusable helper function evaluated entirely at compile time.

template<typename T>
consteval T mask_of_bits(unsigned bits,
                         unsigned lowest_bit = 0);

This function has the luxury of doing terribly inefficient things in service to generality and clarity -- because it's entirely compile time.

This function validates:

  • the target type
  • the width of the mask
  • the starting bit position
  • and the final shifted width

Invalid configurations fail during compilation rather than becoming latent runtime bugs.

Wrapped in a small Mask<T> reusable class with consteval constructors and factories, and some constexpr convenience functions, this gives me:

  • compile-time sanity checking
  • zero runtime overhead
  • and much clearer intent at call sites

If you're into that kind of thing, dive deeper into the exact details of the generated machine code at Godbolt's Compiler Explorer. Something tells me you're into that kind of thing; you've read this far...

Design Pressure (and the “Old Way”)

One interesting part of this exercise was noticing the pull of the traditional approach. In conversations with my AI “thinking buddy,” even when presented with my answer, I got guidance pushing me back to using something like: ( 1<<bits - 1 ).

When pressed, the list of special cases and exceptions started getting added.

Those suggestions aren’t wrong. They’re just optimized for familiarity.

I wanted usage that was readable.

I wanted mask generation that worked correctly for every type, of every width -- signed or unsigned.

I wanted to do this once and be done. At least until the next new C++ feature.

Compile-Time Guarantees

A deliberate design goal was ensuring that all mask construction happens at compile time. If a mask definition is invalid — wrong width, wrong offset, or incompatible type — the error appears during compilation rather than after deployment.

This mirrors how hardware descriptions behave: invalid configurations should not simulate or synthesize.

More room for improvement

Another interesting side note: the error reporting here relies on what is admittedly a bit of a hack (throw inside consteval).

This has the desirable end effect: compile-time error and pointer to the area in code.

However, the compiler error message is misleading: about the use of exceptions in this context. Fortunately, it looks like future C++ standards are improving diagnostics in this area so compile-time error reporting will become clearer.

Closing Thoughts

I'm sure I'll be tweaking this approach as I put it to more real-world use.

This is a small example abstraction, but it reflects a broader trend: modern C++ continues to get better for low-level, hardware-adjacent work. The language keeps providing better tools. The challenge — and the opportunity — is learning when to stop doing things the old way.

/*
* An example showing use of consteval functions and constructors
* to create and use bit masks.
*
* Berk Akinci
* December 17, 2025
*
*/
#ifndef UTILITY_BITMASK_HPP
#define UTILITY_BITMASK_HPP
#include <type_traits>
template<typename T>
consteval T mask_of_bits(const unsigned int bits,
const unsigned int lowest_bit=0) {
// Sanity checks.
constexpr auto bits_in_type = sizeof(T) * 8;
static_assert( std::is_integral_v<T>,
"T must be an integral type." );
if( lowest_bit >= bits_in_type )
throw "Lowest bit must work in type T shift operator.";
if( bits > bits_in_type )
throw "Number of bits must fit in type T.";
if( (lowest_bit + bits) > bits_in_type )
throw "Number of bits (after shift) must fit in type T.";
T mask = 0;
T mask_seed = T{0x1} << lowest_bit;
for( unsigned int bits_to_place = bits;
bits_to_place > 0;
bits_to_place-- ) {
mask <<= 1;
mask |= mask_seed;
}
return mask;
}
template<typename T>
class Mask {
public:
const T mask;
// consteval constructors – entirely compile-time
consteval
Mask(const T mask)
: mask(mask) {}
consteval
Mask(const Mask& other) = default;
// consteval factories
static consteval
Mask of_bits(const unsigned int bits,
const unsigned int lowest_bit=0) {
return Mask(mask_of_bits<T>(bits, lowest_bit));
}
// implicit conversion to T for convenient access
consteval
operator T() const {
return mask;
}
// constexpr Convenience functions.
constexpr
T include(const T value) const {
return (value & mask);
}
constexpr
T exclude(const T value) const {
return (value & ~mask);
}
constexpr
T merge(const T included_value,
const T excluded_value) const {
T merged = 0x0;
merged |= exclude(excluded_value);
merged |= include(included_value);
return merged;
}
};
#endif /* UTILITY_BITMASK_HPP */
/*
* An example showing use of consteval bitmasks.
*
* Berk Akinci
* December 17, 2025
*
*/
#include "BitMask.hpp"
#include <iostream>
#include <format>
#include <array>
using namespace std;
int main() {
using T = uint32_t;
constexpr array tests{
pair{"mask 1b", mask_of_bits<T>( 1)},
pair{"mask 10b", mask_of_bits<T>( 10)},
pair{"mask 0b", mask_of_bits<T>( 0)},
pair{"mask 32b", mask_of_bits<T>( 32)},
pair{"mask 10b @20", mask_of_bits<T>( 10, 20)},
pair{"mask 1b @31", mask_of_bits<T>( 1, 31)},
pair{"mask 0b @31", mask_of_bits<T>( 0, 31)},
// Expected to fail at 32bits:
//pair{"mask 33b", mask_of_bits<T>( 33)},
//pair{"mask 0b @32", mask_of_bits<T>( 0, 32)},
//pair{"mask 2b @31", mask_of_bits<T>( 2, 31)},
};
constexpr array tests_class{
pair{"mask 1b", Mask<T>::of_bits( 1)},
pair{"mask 10b", Mask<T>::of_bits(10)},
pair{"mask 0b", Mask<T>::of_bits( 0)},
pair{"mask 32b", Mask<T>::of_bits(32)},
pair{"mask 10b @20", Mask<T>::of_bits(10, 20)},
pair{"mask 1b @31", Mask<T>::of_bits( 1, 31)},
pair{"mask 0b @31", Mask<T>::of_bits( 0, 31)},
// Expected to fail at 32bits:
//pair{"mask 33b", Mask<T>::of_bits(33)},
//pair{"mask 0b @32", Mask<T>::of_bits( 0, 32)},
//pair{"mask 2b @31", Mask<T>::of_bits( 2, 31)},
};
//constexpr auto impossible_type = Mask<float>::of_bits(1);
auto bits = sizeof(T) * 8;
auto nibbles = sizeof(T) * 2;
cout << "Testing type: " << typeid(T).name()
<< " (" << bits << " bits)\n\n";
for (auto const& [label, value] : tests) {
cout
<< format("{:<15} = {:#0{}x} (binary: {:0{}b})\n",
label, value, nibbles+2, value, bits);
}
cout << endl;
cout << "Testing Class with type: " << typeid(T).name()
<< " (" << bits << " bits)\n\n";
for (auto const& [label, mask] : tests_class) {
cout
<< format("{:<15} = {:#0{}x} (binary: {:0{}b})\n",
label, mask.mask, nibbles+2, mask.mask, bits);
}
cout << endl;
cout << "Testing Class mask/modify with type: " << typeid(T).name()
<< " (" << bits << " bits)\n\n";
constexpr T before = 0x12345678;
constexpr T modify = 0xabcdefab;
for (auto const& [label, mask] : tests_class) {
auto modified = mask.merge(modify, before);
auto copy = mask; // Try copy while we're here.
auto modified_by_copy = copy.merge(modify, before);
if( modified != modified_by_copy )
throw "Copy should behave the same.";
cout
<< format("{:<15} = {:#0{}x} (binary: {:0{}b})\n",
label, modified, nibbles+2, modified, bits);
}
return 0;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment