While working on a low-level network device driver, I needed to do numerous bit-field operations involving masking, shifting, etc. in order to set/clr control bits in various control registers. As there are many registers and many fields to initialize, the canonical way of managing bit fields via C++ &, |, and << operators leads to code that is very error prone and difficult to read and maintain.
I came up with the idea to create a constexpr global constant (MASK_1) that accepts a list of bit-field sizes and offsets and generates a 64-bit constant mask for the defined fields. For example :
auto static constexpr mask = MASK_1 <0 , 4 , 60 , 4> ; // mask = 0xF00000000000000F
While attempting to implement this idea, I get a compile-time error that seems like a bug in the compiler.
My first attempt at defining the MASK_1 variable was to use a variadic template variable of the following :
template <unsigned LSB , unsigned NB , unsigned ... LSB_NB_LIST> requires (1U <= NB && NB <= 64U) && (LSB + NB <= 64U) && (sizeof ... (LSB_NB_LIST) % 2U == 0U)
constexpr uint64_t MASK_1 = (((uint64_t (-1) >> 64 - NB) << LSB) | ... | MASK_1 <LSB_NB_LIST>) ;
This form uses the parameter pack in a fold expression with a left-side initialization and, to my knowledge, is valid in C++20.
Trying to compile this yields an error message that the recursive reference parameter pack is too short. I have confirmed my test cases to have exactly 2, 4, or 6 arguments leading to 0 , 2 , or 4 parameter pack arguments, so there is no mismatch in the number of expected arguments.
I tried rewriting the MASK_1 constant using a lambda according to the following and it works fine :
template <unsigned LSB , unsigned NB , unsigned ... LSB_NB_LIST> requires (1U <= NB && NB <= 64U) && (LSB + NB <= 64U) && (sizeof ... (LSB_NB_LIST) % 2U == 0U)
constexpr uint64_t MASK_1 = [] (void) noexcept -> uint64_t
{
auto const mask_1 = (uint64_t (-1) >> 64 - NB) << LSB ;
if constexpr (sizeof ... (LSB_NB_LIST) == 0)
{
return mask_1 ;
}
else
{
return mask_1 | MASK_1 <LSB_NB_LIST ...> ;
}
} () ;
I also tried to use a fold expression in the second attempt as below, but it too generates an error :
template <unsigned LSB , unsigned NB , unsigned ... LSB_NB_LIST> requires (1U <= NB && NB <= 64U) && (LSB + NB <= 64U) && (sizeof ... (LSB_NB_LIST) % 2U == 0U)
constexpr uint64_t MASK_1 = [] (void) noexcept -> uint64_t
{
auto const mask_1 = (uint64_t (-1) >> 64 - NB) << LSB ;
if constexpr (sizeof ... (LSB_NB_LIST) == 0)
{
return mask_1 ;
}
else
{
return (mask_1 | ... | MASK_1 <LSB_NB_LIST>) ;
}
} () ;
At this point, it appears that there is a bug in my compiler (GCC 11.2.0).
Can anyone confirm if this is a compiler bug or if I am somehow incorrectly doing the fold expression syntax in the first and third attempts?
EDIT:
Here is the minimal test code :
#include <cstdio>
#include <cstdint>
template <unsigned LSB , unsigned NB , unsigned ... LSB_NB_LIST> requires (1U <= NB && NB <= 64U) && (LSB + NB <= 64U) && (sizeof ... (LSB_NB_LIST) % 2U == 0U)
constexpr uint64_t MASK_1 = (((uint64_t (-1) >> 64 - NB) << LSB) | ... | MASK_1 <LSB_NB_LIST>) ;
int main (int argc , char * args [])
{
printf ("%016llX\n" , MASK_1 <0 , 4>) ; // Works
printf ("%016llX\n" , MASK_1 <0 , 4 , 60 , 4>) ; // Error
}
Here is the compiler call :
g++ --std=c++20 -I. -o m.exe main.cpp
Here is the compiler error message :
main.cpp: In instantiation of 'constexpr const uint64_t MASK_1<0, 4, 60, 4>':
main.cpp:12:25: required from here
main.cpp:6:74: error: wrong number of template arguments (1, should be at least 2)
6 | constexpr uint64_t MASK_1 = (((uint64_t (-1) >> 64 - NB) << LSB) | ... | MASK_1 <LSB_NB_LIST>) ;
| ^~~~~~~~~~~~~~~~~~~~
main.cpp:6:20: note: provided for 'template<unsigned int LSB, unsigned int NB, unsigned int ...LSB_NB_LIST> requires 1 <= NB && NB <= 64 && LSB + NB <= 64 && sizeof ... ((LSB_NB_LIST ...)) % 2 == 0 constexpr const uint64_t MASK_1<LSB, NB, LSB_NB_LIST ...>'
6 | constexpr uint64_t MASK_1 = (((uint64_t (-1) >> 64 - NB) << LSB) | ... | MASK_1 <LSB_NB_LIST>) ;
| ^~~~~~
EDIT-2 : Lambda method that works
#include <cstdio>
#include <cstdint>
template <unsigned LSB , unsigned NB , unsigned ... LSB_NB_LIST> requires (1U <= NB && NB <= 64U) && (LSB + NB <= 64U) && (sizeof ... (LSB_NB_LIST) % 2U == 0U)
#if 0
constexpr uint64_t MASK_1 = (((uint64_t (-1) >> 64 - NB) << LSB) | ... | MASK_1 <LSB_NB_LIST>) ;
#else
constexpr uint64_t MASK_1 = [] (void) noexcept -> uint64_t
{
auto const mask_1 = (uint64_t (-1) >> 64 - NB) << LSB ;
if constexpr (sizeof ... (LSB_NB_LIST) == 0)
{
return mask_1 ;
}
else
{
return mask_1 | MASK_1 <LSB_NB_LIST ...> ;
}
} () ;
#endif
int main (int argc , char * args [])
{
printf ("%016llX\n" , MASK_1 <0 , 4>) ; // Works
printf ("%016llX\n" , MASK_1 <0 , 4 , 60 , 4>) ; // Works
}
The issue here, as I have come to understand, is a mismatch between what I think should be happening vs what is actually happening.
The original expression :
constexpr uint64_t MASK_1 = (((uint64_t (-1) >> 64 - NB) << LSB) | ... | MASK_1 <LSB_NB_LIST>) ;
This is actually doing the following :
constexpr uint64_t MASK_1 = (uint64_t (-1) >> 64 - NB) << LSB | MASK_1 <LSB_1> | MASK_1 <NB_1> | MASK_1 <LSB_2> | MASK_1 <NB_2> | ... ;
instead of what I thought it was supposed to do :
constexpr uint64_t MASK_1 = ((uint64_t (-1) >> 64 - NB) << LSB | MASK_1 <LSB_1 , NB_1> | MASK_1 <LSB_2 , NB_2> | ... ;
Which is why the compiler emits the correct error message stating the wrong number of arguments for MASK_1 in the binary fold expression.
For the lambda method, the recursive call to MASK_1 actually passes the whole parameter pack, so is correct.