A basic multi-dimensional array
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.