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?

1
  • 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. Commented Apr 21, 2021 at 2:58

2 Answers 2

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.

Sign up to request clarification or add additional context in comments.

Comments

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>());
}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.