Search code examples
templatesvariablesvariadic-templatesfold

Variadic template variable parameter pack expansion in recursive variable definition has incorrect size


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
}


Solution

  • 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.