PaperBirdMaster
PaperBirdMaster

Reputation: 13298

Sub-array template

Let me explain what I'm looking for with code. Let's supose we have a template wrapping an array:

template <typename T, std::size_t SIZE>
struct wrap
{
    using value_type = T;
    using wrap_type = wrap;

    static constexpr auto size = SIZE;
    T data[size]{};
};

I would like to add a function to the wrap template, this function should return an object able to manipulate a specific part of the array contained in wrap, the function should know where the sub-array starts and how many elements it will have so I thought about a function like this:

template <typename T, std::size_t SIZE>
struct wrap
{
    using value_type = T;
    using wrap_type = wrap;

    static constexpr auto size = SIZE;
    T data[size]{};

    template <std::size_t START, std::size_t COUNT>
    ???? sub() { ... }
};

I don't want the ???? object returned by wrap::sub to have a couple of pointers but an static array, so my approach was:

template <typename T, std::size_t SIZE>
struct wrap
{
    using value_type = T;
    using wrap_type = wrap;

    static constexpr auto size = SIZE;
    T data[size]{};

    template <std::size_t START, std::size_t COUNT>
    struct sub_array
    {
        using value_type = wrap_type::value_type *;

        static constexpr auto start = START;
        static constexpr auto count = COUNT;

        value_type data[count]{};
    };

    template <std::size_t START, std::size_t COUNT>
    sub_array<START, COUNT> sub() { ... }
};

And here is the problem: What should I write into wrap::sub? I'm pretty sure that an approach using std::integer_sequence should be possible but I lack on experience with it hence I don't even know what to try. I've tried to asume a value of 2 with both template parameters in wrap::sub and it worked as expected:

template <std::size_t START, std::size_t SIZE>
sub_array<START, SIZE> sub() { return { &data[START], &data[START + 1u] }; }

I've tested it as I show below:

wrap<int, 9> w{1, 2, 3, 4, 5, 6, 7, 8, 9};

// x is the view [3, 4]
auto x = w.sub<2, 2>();

std::cout << *x.data[0] << '\n'; // shows 3

So, conceptually the body of wrap::sub should be:

return { &data[START + 0], &data[START + 1], ... &data[START + n] };

How should I achieve the effect above?

Upvotes: 2

Views: 232

Answers (2)

Yakk - Adam Nevraumont
Yakk - Adam Nevraumont

Reputation: 275966

This solves the problem as described above:

template<class=void, std::size_t...Is>
auto indexer( std::index_sequence<Is...> ) {
  return [](auto&& f){
    return decltype(f)(f)( std::integral_constant<std::size_t, Is>{}... );
  };
}
template<std::size_t N>
auto indexer() {
  return indexer( std::make_index_sequence<N>{} );
}

This lets you create an expandable parameter pack of compile-time integers without having to write a custom helper function each time.

It is valid C++14, but some compilers (like MSVC) claim to be C++14 compilers without actually being C++14 compilers.

template <std::size_t START, std::size_t SIZE>
sub_array<START, SIZE> sub() const {
  auto index = indexer<SIZE>();
  return index(
    [&](auto...Is)->sub_array<START, SIZE>
    {
      return {data[START+Is]...};
    }
  );
}

indexer<N> returns an indexer that when passed a lambda, invokes it with std::integral_constant<std::size_t, 0> through std::integral_constant<std::size_t, N-1>.

This pack can then be expanded inline in the function that creates the indexer.


In the comments you mention you want modifications to the sub-array to be reflected in the original array.

Your design does not permit this. The sub-array is copy of a slice of the original array.

The right way to do that is for your sub-array to be a pair of pointers into your original array.

template<class T>
struct array_view {
  T* b = 0;
  T* e = 0;

  T* begin() const { return b; }
  T* end() const { return e; }

  T& operator[](std::size_t i)const { return begin()[i]; }

  bool empty() const { return begin()==end(); }
  std::size_t size() const { return end()-begin(); }

  array_view( T* s, T* f ):b(s), e(f) {}
  array_view( T* s, std::size_t l):array_view(s, s+l) {}

  array_view()=default;
  array_view(array_view const&)=default;
  array_view& operator=(array_view const&)=default;
};

An array_view is a compact way to talk about a slice of an array.

If you want stride, you have to do a bit more work.

Upvotes: 1

Sergey
Sergey

Reputation: 8248

I'm pretty sure that an approach using std::integer_sequence should be possible...

Here it is:

template <typename T, std::size_t SIZE>
struct wrap
{
    using value_type = T;
    using wrap_type = wrap;

    static constexpr auto size = SIZE;
    T data[size]{};

    template <std::size_t START, std::size_t COUNT>
    struct sub_array
    {
        using value_type = wrap_type::value_type *;

        static constexpr auto start = START;
        static constexpr auto count = COUNT;

        value_type data[count]{};
    };

    template <std::size_t START, std::size_t COUNT>
    constexpr sub_array<START, COUNT> sub() {
        static_assert(START + COUNT <= SIZE, "sub_array is out of range");
        // The argument makes an instance of integer sequence from 0 to COUNT-1
        return sub_impl<START, COUNT>(std::make_index_sequence<COUNT>{});
    }

private:

    template <std::size_t START, std::size_t COUNT, std::size_t ... Is>
    constexpr sub_array<START, COUNT> sub_impl(std::index_sequence<Is...>) {
        // Arithmetic in argument list expansion is pretty valid.
        return {&data[Is+START]...};
    }
};

Now the following lines:

wrap<double, 10> ds{1,2,3,4,5,6,7,8,9,10};
auto s = ds.sub<2,3>();
std::cout << *s.data[0] << std::endl;

print 3.

This code is exactly an adaptation of documentation examples for your structure. Probably, it may be shortened. And I'm sure that more static (compile-time) assertions may be declared in your code.

Finally, following @Yakk's comments, this implementation of sub returns a subarray copy, not a view.

Upvotes: 1

Related Questions