0

Say I want to write a function arrfill<N> that fills an array of length N. Below is the template implementation I tried.

template<typename T>
bool arrfill(T arr[0], T v){;}

template<size_t N, typename T>
void arrfill(T arr[N], T v){
    arr[0] = v;
    arrfill<N-1>(arr+1, v);
}

int main(int argc, char const *argv[])
{
    bool barr[4];
    arrfill<4>(barr, true);
}

However, this won't compile as the template instantiation will not terminate at the case when N is 0 and will exceed its maximum depth.

It seems like the compilers won't take the array size in the signature as the argument type. I wonder what's the proper way for specifying that?

Yangcy
  • 167
  • 1
  • 6
  • 1
    FYI, `std::array` has a `fill` function. `std::fill` works on arrays as well as any other iterator-compatible (or range-compatible for `std::ranges::fill` in C++20) type. – chris Apr 21 '21 at 02:58

2 Answers2

2

You are bitten by argument decay.

Argument decay means that int arr[N] is fancy talk for int* arr. The N is utterly ignored.

On top of that, arrfill<N-1> is a call to a template function whose first argument is a size_t (or compatible). Your T arr[0] is an overload that takes as its first template argument a type. So it cannot be selected.

To work around argument decay, you should take arrays by reference.

template<typename T>
bool arrfill(T (&arr)[1], T v){arr[0]=v;}

template<size_t N, typename T>
void arrfill(T (&arr)[N], T v){
  arr[0] = v;
  arrfill(*reinterpret_cast<T(*)[N-1]>(arr+1), v);
}

Sadly, this is undefined behavior; I am casting parts of an array into arrays of type it isn't. This happens to be undefined behavior which every single C++ compiler I have ever used consumes and does "the right thing", but it is still undefined behavior. And we should avoid that unless we have a strong reason not to; while clean and clear, clean code is not (in my opinion) a good reason to do UB. UB can come back to bite us 10 years from now when compilers update, and I don't want to maintain this code every compiler update and ensure it still works.

So really, use packs and folding.

template<size_t N, typename T,std::size_t...Is>
void arrfill(T (&arr)[N], T v,std::index_sequence<Is...>){
  ((void)(arr[Is]=v),...);
}
template<size_t N, typename T>
void arrfill(T (&arr)[N], T v){
  arrfill(arr, v, std::make_index_sequence<N>{});
}

or just use std::fill_n.

template<size_t N, typename T>
void arrfill(T (&arr)[N], T v){
  std::fill_n( std::begin(arr), N, v );
}

if you really, really must use recursion

template<size_t N, typename T>
void arrfill(T* arr, T v){
  if constexpr(N==0) {
    return;
  } else {
    arr[0] = v;
    arrfill<N-1>(arr+1, v);
  }
}

does it. In we can't use if constexpr. So we do something else.

template<typename T>
void arrfill(std::integral_constant<std::size_t, 0>, T* arr, T const& v){
}

template<size_t N, typename T>
void arrfill(std::integral_constant<std::size_t, N>, T* arr, T const& v){
  arr[0] = v;
  arrfill(std::integral_constant<std::size_t, N-1>{}, arr+1, v);
}

template<size_t N, typename T>
void arrfill(T(&arr)[N], T const& v){
  arrFill(std::integral_constant<std::size_t, N>{}, arr, v);
}

this lets us select the 0 case using overloading. We also deduce N automatically.

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
1

I came out with a solution by defining a new class encoding the length information. But I wonder whether it's the most elegant way.

template<size_t N>
struct intClass{};

template<typename T>
bool arrfill(T arr[0], T v, intClass<0>){;}

template<size_t N, typename T>
void arrfill(T arr[N], T v, intClass<N>){
    arr[0] = v;
    arrfill(arr+1, v, intClass<N-1>());
}

template<size_t N, typename T>
void arrfill(T arr[N], T v){
    arrfill(arr,v, intClass<N>());
}
Yangcy
  • 167
  • 1
  • 6