Search code examples
c++templatesc++17template-argument-deduction

Template argument deduction with parameter list initialisation


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?


Solution

  • 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;
     }