2
votes

I have been trying to create a class which represents a non-owning, multidimensional view of an array (sort of like an N-dimensional std::string_view), where the dimensionality is varied "dynamically". Ie the number of dimensions and dimension sizes are not tied to the class, but specified when elements are accessed (through operator()). The following code sums up the functionality I'm looking for:

#include <array>
#include <cstddef>

template<typename T>
struct array_view {

    T* _data;

    // Use of std::array here is not specific, I intend to use my own, but similar in functionality, indices class.
    template<std::size_t N>
    T& operator()(std::array<std::size_t, N> dimensions, std::array<std::size_t, N> indices) const
    {
        std::size_t offset = /* compute the simple offset */;

        return _data[offset];
    }

};

int main()
{
    int arr[3 * 4 * 5] = {0};

    array_view<int> view{arr};

    /* Access element 0. */
    // Should call array_view<int>::operator()<3>(std::array<std::size_t, 3>, std::array<std::size_t, 3>)
    view({5, 4, 3}, {0, 0, 0}) = 1;
}

However this fails to compile (ignoring the obvious syntax error in operator()) with

main.cpp: In function 'int main()':
main.cpp:28:27: error: no match for call to '(array_view<int>) (<brace-enclosed initializer list>, <brace-enclosed initializer list>)'
  view({5, 4, 3}, {0, 0, 0}) = 1;
                           ^
main.cpp:11:5: note: candidate: 'template<long unsigned int N> T& array_view<T>::operator()(std::array<long unsigned int, N>, std::array<long unsigned int, N>) const [with long unsigned int N = N; T = int]'
  T& operator()(std::array<std::size_t, N> dimensions, std::array<std::size_t, N> indices) const
     ^~~~~~~~
main.cpp:11:5: note:   template argument deduction/substitution failed:
main.cpp:28:27: note:   couldn't deduce template parameter 'N'
  view({5, 4, 3}, {0, 0, 0}) = 1;
                           ^

I am not an expert on template instantiation/deduction. However it would seem to me that the compiler tries to deduce N from std::initializer_list<int> arguments, which fails because operator() is declared to take std::array<std::size_t, N> arguments. Hence compilation fails.

Doing another, far more simplified, experiment, shows similar results:

template<typename T>
struct foo {
    T val;
};

struct bar {
    template<typename T>
    void operator()(foo<T>) {}
};

int main()
{
    bar b;
    b({1});
}

Output:

main.cpp: In function 'int main()':
main.cpp:14:7: error: no match for call to '(bar) (<brace-enclosed initializer list>)'
  b({1});
       ^
main.cpp:8:10: note: candidate: 'template<class T> void bar::operator()(foo<T>)'
     void operator()(foo<T>) {}
          ^~~~~~~~
main.cpp:8:10: note:   template argument deduction/substitution failed:
main.cpp:14:7: note:   couldn't deduce template parameter 'T'
  b({1});

It seems the compiler does not even get to trying to convert {1} (which is a valid initialisation of foo<int>) to foo<int> because it stops after failing to deduce the function template argument.

So is there any way to achieve the functionality I'm looking for? Is there some new syntax I'm missing, or an alternate approach that does the same, or is it simply not possible?

2

2 Answers

2
votes

So is there any way to achieve the functionality I'm looking for? Is there some new syntax I'm missing, or an alternate approach that does the same, or is it simply not possible?

Obviously you can explicit the N value as follows

view.operator()<3U>({{5U, 4U, 3U}}, {{0U, 0U, 0U}}) = 1;

but I understand that's a ugly solution.

For an alternative approach... if you can accept to renounce the second array (and, calling the operator, with a second initializer list) and if is OK for you the use of a variadic template list... the size of the variadic list become the dimension of the surviving array

template <typename ... Ts>
T & operator() (std::array<std::size_t, sizeof...(Ts)> const & dims,
                Ts const & ... indices) const
 {
    std::size_t offset = 0 /* compute the simple offset */;

    return _data[offset];
 }

and you can use it as follows

view({{5U, 4U, 3U}}, 0U, 0U, 0U) = 1;

Obviously the use of indices, inside the operator, can be more complicated and can be necessary add some check about Ts... type (to verify that all of they are convertible to std::size_t

But I suppose you can also define a func() method as your original operator()

template <std::size_t N>
T & func (std::array<std::size_t, N> const & dims,
          std::array<std::size_t, N> const & inds) const
 {
   std::size_t offset = 0 /* compute the simple offset */;

   return _data[offset];
 }

and you can call it from operator()

template <typename ... Ts>
T & operator() (std::array<std::size_t, sizeof...(Ts)> const & dims,
                Ts const & ... indices) const
 { return func(dims, {{ indices... }}); }

The following is a full working example

#include <array>
#include <cstddef>

template <typename T>
struct array_view
 {
   T * _data;

   template <std::size_t N>
   T & func (std::array<std::size_t, N> const & dims,
             std::array<std::size_t, N> const & inds) const
    {
      std::size_t offset = 0 /* compute the simple offset */;

      return _data[offset];
    }


   template <typename ... Ts>
   T & operator() (std::array<std::size_t, sizeof...(Ts)> const & dims,
                   Ts const & ... indices) const
    { return func(dims, {{ indices... }}); }

 };

int main ()
 {
   int arr[3 * 4 * 5] = {0};

   array_view<int> view{arr};

   view({{5U, 4U, 3U}}, 0U, 0U, 0U) = 1;
 }
1
votes

The reason it fails to compile is that things like {5, 4, 3} and {0, 0, 0} (called braced-init-lists) are not like first-class expressions. They don't have types. In template deduction, we try to match a type to an expression - and we can't do that against a braced-init-list. And even if we could, a braced-init-list isn't a std::array of any kind, so that wouldn't match. This is something that we'd need additional language support for that we just don't have.

There are two exceptions.

The big one is std::initializer_list<T>. But that has runtime size which we don't want in this case, since you want to enforce that the dimensions and indices parameters have the same size (presumably).

The other one is raw arrays. You can deduce T[N] from a braced-init-list. So you can write something like:

template <typename D, typename I, std::size_t N>
T& operator()(D const (&dimensions)[N], I const (&indices)[N]) const;

That will let you write view({5, 4, 3}, {0, 0, 0}), which will deduce D and I as int and N as 3. It will also correctly prevent view({5, 4, 3}, {0}) and view({5}, {0, 0}) from compiling.

You may want to add additional constraints that D and I are integral types.