Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Initialize std::array by parameter pack from arbitrary index

Initializing an std::array by variadic template arguments, starting from a given index can be done the following way:

#include <array>

template <typename T, size_t N>
struct A
{
   template <typename ... Ts>
   A(size_t i, Ts ... vals)
   {
      constexpr size_t P = sizeof...(vals);
      std::array<T, P> temp{ vals... };

      for (size_t j = 0; j < P; ++j)
      {
         arr[i + j] = temp[j];
      }
   }

   std::array<T, N> arr;
};

But is it possible to achieve the same without converting the parameter pack to a temporary tuple or another std::array?

like image 312
plasmacel Avatar asked Mar 28 '16 15:03

plasmacel


3 Answers

You may use std::index_sequence and delegating constructor:

template <typename T, size_t N>
struct A
{
   template <typename ... Ts>
   A(size_t i, Ts&& ... vals)
   :
       A(std::index_sequence_for<Ts...>{}, i, std::forward<Ts>(vals)...)
   {}

   template <std::size_t...Is, typename ... Ts>
   A(std::index_sequence<Is...>, size_t i, Ts&& ... vals)
   {
      int dummy[] = {0, ((arr[i + Is] = vals), void(), 0)...};
      static_cast<void>(dummy); // Avoid warning for unused variable
   }

   std::array<T, N> arr;
};

Or with Folding expression of C++17,

   template <std::size_t...Is, typename ... Ts>
   A(std::index_sequence<Is...>, size_t i, Ts&& ... vals)
   {
      (static_cast<void>(arr[i + Is] = vals), ...);
   }
like image 143
Jarod42 Avatar answered Nov 10 '22 01:11

Jarod42


Although it involves C++17 fold expressions, the simplest possible solution inspired by the comment of Jarod42 is:

#include <array>

template <typename T, size_t N>
struct A
{
   template <typename ... Ts>
   A(Ts ... vals)
   {
     size_t i = 1; // starting index, can be an argument
     ((arr[i++] = vals) , ...);
   }

   std::array<T, N> arr;
};

int main()
{
   A<int, 4> a(1, 2, 3);

   return 0;
}

Compiling this with Clang 3.6 or above with optimization level -O1 and -std=c++1z flag the generated assembly is:

A<int, 4ul>::A<int, int, int>(int, int, int):             # @A<int, 4ul>::A<int, int, int>(int, int, int)
pushq   %rbp
pushq   %r15
pushq   %r14
pushq   %rbx
pushq   %rax
movl    %ecx, %r14d
movl    %edx, %r15d
movl    %esi, %ebx
movq    %rdi, %rbp
movl    $1, %esi
callq   std::array<int, 4ul>::operator[](unsigned long)
movl    %ebx, (%rax)
movl    $2, %esi
movq    %rbp, %rdi
callq   std::array<int, 4ul>::operator[](unsigned long)
movl    %r15d, (%rax)
movl    $3, %esi
movq    %rbp, %rdi
callq   std::array<int, 4ul>::operator[](unsigned long)
movl    %r14d, (%rax)
addq    $8, %rsp
popq    %rbx
popq    %r14
popq    %r15
popq    %rbp
retq

Which is equivalent to the test case

template <typename T, size_t N>
struct B
{
   B(T x, T y, T z)
   {
     arr[1] = x;
     arr[2] = y;
     arr[3] = z;
   }

   std::array<T, N> arr;
};

int main()
{
   B<int, 4> b(1, 2, 3);

   return 0;
}

with generated assembly

B<int, 4ul>::B(int, int, int):                     # @B<int, 4ul>::B(int, int, int)
pushq   %rbp
pushq   %r15
pushq   %r14
pushq   %rbx
pushq   %rax
movl    %ecx, %r14d
movl    %edx, %r15d
movl    %esi, %ebx
movq    %rdi, %rbp
movl    $1, %esi
callq   std::array<int, 4ul>::operator[](unsigned long)
movl    %ebx, (%rax)
movl    $2, %esi
movq    %rbp, %rdi
callq   std::array<int, 4ul>::operator[](unsigned long)
movl    %r15d, (%rax)
movl    $3, %esi
movq    %rbp, %rdi
callq   std::array<int, 4ul>::operator[](unsigned long)
movl    %r14d, (%rax)
addq    $8, %rsp
popq    %rbx
popq    %r14
popq    %r15
popq    %rbp
retq

It's worth to note that the generated assembly of the two case is equivalent.

like image 34
plasmacel Avatar answered Nov 10 '22 00:11

plasmacel


template<std::size_t I>
using index_t = std::integral_constant<std::size_t, I>;

template<class T, std::size_t=0, class...Args>
T make(Args&&...args){ return T(std::forward<Args>(args)...); }

template <class T, size_t N>
struct A {
  template <std::size_t I, class...Ts>
  A(index_t<I>, Ts&&... vals):
    A(std::make_index_sequence<I>{}, std::forward<Ts>(vals)...)
  {}
  template <std::size_t...Is, class...Ts>
  A(std::index_sequence<Is...>, Ts&&...vals):
    arr{{ make<T,Is>()..., std::forward<Ts>(vals)... }}
  {}
  // ...
};

this requires a compile-time known offset, but avoids creating a default-constructed array then assigning to it. It however also involves creating a pile of Ts and moving them into the array. Ick!

Backing up a second, we know that the array is a standard layout block of Ts. Let's cheat.

// stores the raw buffer for the array, and constructs it with
// some care:
template<class T>
using storage = std::aligned_storage_t<sizeof(T),alignof(T)>;
template<class T, size_t N>
struct A_storage {
  storage<std::array<T, N>> raw_arr;
  std::array<T,N>& arr() {
    return *reinterpret_cast<std::array<T, N>*>(&raw_arr);
  }
  std::array<T,N> const& arr() const {
    return *reinterpret_cast<std::array<T, N> const*>(&raw_arr);
  }

  template<class...Ts>
  static void make_arr(storage<std::array<T, N>>& retval, std::size_t offset, Ts&&...ts) {
    auto* ptr = reinterpret_cast<T*>(&retval);
    try {
      std::size_t ctor_count = 0;
      for (size_t i = 0; i < offset; ++i) {
        ++ctor_count;
        ::new((void*)(ptr+i)) T();
      }
      ::new((void*)(ptr +offset)) std::array<T, sizeof...(Ts)>{{
        (++ctor_count, void(), std::forward<Ts>(ts))...
      }};
      for (size_t i = offset+sizeof...(Ts); i < N; ++i) {
        ++ctor_count;
        ::new((void*)(ptr+i)) T();
      }
    } catch(...) {
      // ctor_count is the count of constructors *attempted*
      // so ptr[ctor_count-2] is the last one we *succeeded at*
      // destroy everything we successfully constructed.
      // ctor_count has to be at least 1, as we cannot throw before
      // incrementing.  Let us peel it off.
      --ctor_count;
      // iterate backwards from ctor_count-1 down to 0, so we destroy
      // in reverse order of constructing:
      for (size_t i = 1; i <= ctor_count; ++i) {
        ptr[ctor_count-i].~T();
      }
      throw;
      // I use attempted, because the initializer list syntax of array
      // construction doesn't let me increment after I provide the value
      // for the place I'm constructing.  I can, however, increment before.

    }
  }
  template<class...Ts>
  A_storage(std::size_t offset, Ts&&...ts)
  {
    static_assert(sizeof...(Ts)<=N, "too much data!");
    ASSERT(offset+sizeof...(Ts)<=N);
    make_arr(raw_arr, offset, std::forward<Ts>(ts)...);
  }
  // only runs if the ctor above completes, which means
  // everything was constructed:
  ~A_storage() {
    for (size_t i = 1; i <= N; ++i) {
      arr()[N-i].~T();
    }
  }
};
template<std::size_t N, class T>
struct A:A_storage {
  template<class...Ts>
  A(std::size_t offset, Ts&&...ts):
    A_storage(offset, std::forward<Ts>(ts)...)
  {}
};

Everything is constructed in-place. No need for T(T&&) support! (unless that is how you pass in the parameters to the array, but that is your problem not mine)

I tried to make destruction work in an exception-heavy environment. I may have gotten it wrong.

The only temporary variable (the return value of make_arr) should be elided by NRVO. On the other hand, a compiler might be a slacker and not elide into class member constructors. Bad compiler.

like image 45
Yakk - Adam Nevraumont Avatar answered Nov 10 '22 01:11

Yakk - Adam Nevraumont