A basic multi-dimensional array I have been lurking on the C++ side of stack-overflow only long enough to know there are a lot of beginners and intermediate programmers baffled by multi-dimensional arrays. I've seen a lot of monstrosities : three-star programmers, cache-unfriendly implementations, vectors of vectors of vectors ..., etc.

I thought to provide them with a basic multi-dimensional array:


open for extensions,
fit for production and experimentation, 
working without allocations,
and cache-friendly.


Before I do so, I'd like some advice from the community.


  
  What can I improve in the provided features of this class?
  What new feature could one add, and why would it be useful?
  


#include &lt;array&gt;
#include &lt;numeric&gt;

namespace ysc
{
namespace _details
{
    template&lt;class InputIt, class OutputIt&gt;
    OutputIt partial_product(InputIt first, InputIt last, OutputIt output)
    { *output++ = 1; return partial_sum(first, last, output, std::multiplies&lt;&gt;{}); }

    // cache-friendly:
    // neighbor objects within the right-most coordinate are neighbors in memory
    template&lt;class TDim, class TCoord&gt;
    auto coordinates_to_index(TDim const&amp; dimensions, TCoord const&amp; coords)
    {
        std::array&lt;std::size_t, dimensions.size()&gt; dimension_product;
        using std::crbegin, std::crend, std::prev;
        partial_product(crbegin(dimensions), prev(crend(dimensions)), begin(dimension_product));
        return std::inner_product(cbegin(dimension_product), cend(dimension_product), crbegin(coords), 0);
    }
}

constexpr struct matrix_zero_t {} matrix_zero;

template&lt;class T, std::size_t... Dimensions&gt;
class matrix
{
template&lt;class, std::size_t...&gt; friend class matrix;

public:
    static constexpr std::size_t order      = sizeof...(Dimensions);
    static constexpr std::array  dimensions = { Dimensions... };

private:
    std::array&lt;T, (Dimensions * ...)&gt; _data;

public:
    friend void swap(matrix&amp; lhs, matrix&amp; rhs)
    {
        using std::swap;
        swap(lhs._data, rhs._data);
    }

public:
    matrix()                          = default;
    matrix(matrix&amp;&amp; other)            = default;
    matrix&amp; operator=(matrix&amp;&amp; other) = default;

    matrix(matrix_zero_t) : _data({}) {}

    template&lt;class U&gt;
    matrix(matrix&lt;U, Dimensions...&gt; const&amp; other) { std::copy(cbegin(other._data), cend(other._data), begin(_data)); }

    template&lt;class U&gt;
    matrix&amp; operator=(matrix&lt;U, Dimensions...&gt; const&amp; other)
    {
        matrix o{other};
        swap(*this, o);
        return *this;
    }

public:
    template&lt;class... Args&gt;
    T const&amp; operator()(Args... coordinates) const
    { return _data[_details::coordinates_to_index(dimensions, std::array{coordinates...})]; }

    template&lt;class... Args&gt;
    T&amp; operator()(Args... coordinates)
    { return _data[_details::coordinates_to_index(dimensions, std::array{coordinates...})]; }
};
} // namespace ysc


Usage demo: http://coliru.stacked-crooked.com/a/1652451c78275436

This is a C++17 implementation; this itself is not set in stone.
Here are some suggestions:


coordinates_to_index should return std::size_t instead of int because int may not be able to hold the required values.

return std::inner_product(cbegin(dimension_product), cend(dimension_product),
                          crbegin(coords), std::size_t{0}); // instead of int

You don't need to compute the strides and store them. You can use Horner's rule directly.

std::size_t result = 0;
for (std::size_t i = 0; i &lt; N; ++i)
    result = result * dimensions[i] + coords[i];
return result;

Your calculation of the total size fails to consider the case where order == 0. Instead of 

(Dimensions * ...)                   // ill-formed if Dimensions is empty


Use

(Dimensions * ... * std::size_t{1})


Moreover, I would suggest exposing the total size, along with the order and dimensions. Like this:

static constexpr std::size_t size = (Dimensions * ... * std::size_t(1));


And use it in place of ad-hoc computations further in code.
Your matrix is not Copyable. You declare move operations but not copy operations (the template doesn't make a difference), and the compiler synthesizes deleted copy operations which take precedence over the copying template. You should either default the copy operations explicitly (and maybe destructor as well), or omit the redundant default declarations directly.
Why use :_data({}) when :_data{} is more readable and less verbose?
Consider adding out-of-range detection for operator() in some form.


Update


Currently, the converting copy constructor first default-initializes the elements and then copy assign from the initializer matrix, which is kinda surprising. It is more reasonable to initialize directly (with some template hacks):

namespace detail {
    template &lt;class T, std::size_t N, class U, std::size_t... Is&gt;
    std::array&lt;T, N&gt; construct(const std::array&lt;U, N&gt;&amp; arr, std::index_sequence&lt;Is...&gt;)
    {
        return std::array&lt;T, N&gt;{static_cast&lt;T&gt;(arr[Is])...};
    }
}

template &lt;class T, std::size_t... Dimensions&gt;
class matrix {
    // ...
public:
    // ...
    template &lt;class U&gt;
    matrix(matrix&lt;U, Dimensions...&gt; const&amp; other)
        : _data{detail::construct&lt;T, N&gt;(other._data, std::make_index_sequence&lt;size&gt;{})}
    {
    }
    // ...
};


The above implementation requires access to relies on access to _data of another matrix specialization, which can be granted with a friend declaration or an access function. It also relies on the C++17 guaranteed copy elision.

Now, the converting copy assignment operator cannot delegate to the converting copy constructor:

template &lt;class U&gt;
matrix&amp; operator=(const matrix&lt;U, Dimensions...&gt;&amp; other)
{
    std::copy_n(_data.begin(), size, other._data.begin());
    return *this;
}

It makes sense to constrain the converting copy constructor and the converting copy assignment operator with SFINAE. Also, they should be made conditionally explicit and conditionally noexcept:

template &lt;class U, class = std::enable_if_t&lt;
    std::is_constructible_v&lt;T, const U&amp;&gt; &amp;&amp; std::is_convertible_v&lt;const U&amp;, T&gt;
&gt;&gt; matrix(matrix&lt;U, Dimensions...&gt; const&amp; other)
    noexcept(std::is_nothrow_constructible_v&lt;T, const U&amp;&gt;);

template &lt;class U, class = std::enable_if_t&lt;
    std::is_constructible_v&lt;T, const U&amp;&gt; &amp;&amp; !std::is_convertible_v&lt;const U&amp;, T&gt;
&gt;&gt; explicit matrix(matrix&lt;U, Dimensions...&gt; const&amp; other)
    noexcept(std::is_nothrow_constructible_v&lt;T, const U&amp;&gt;);


Similarly for the converting copy assignment operator.
Consider support converting move construction and converting move assignment.
The class can be modified to support constexpr.