Flatten 2d-array at compiletime In my code, I need to flatten a two-dimensional std::array into a one-dimensional one at compile time. While writing the code I realized, that I am unsure about several aspects of my implementation (as far as readability is concerned). 
Therefore I would really appreciate your input!
I am especially unsure, if 


There is a more obvious / simpler version in which I can achieve my goal (I previously thought of expanding multiple parameter packs with one ... or the like)
if using a nested lambda is a good idea (I do this in order to obtain access to the parameter pack created by the integer_sequence and not expose this API)
If using the requires expression is really more readable than using a std::enable_if_t in this use case
and, even if I kind of like my solution to this problem, whether there is a cleaner way of


having automatic template parameter deduction for the "exposed" API
having access to sizes and type of the nested array

If there are other things I can improve with my code


#include &lt;array&gt;
#include &lt;iostream&gt;

/// Helper struct, which is std::false_type if the type is not a nested array, and otherwise holds
/// inner and outer array sizes.
template&lt;typename Arr&gt; struct NestedArray : std::false_type{};
template &lt;std::size_t sizeOuter, std::size_t sizeInner, typename T&gt;
struct NestedArray&lt;std::array&lt;std::array&lt;T, sizeInner&gt;, sizeOuter&gt;&gt; : std::true_type{
  static constexpr auto outer = sizeOuter; 
  static constexpr auto inner = sizeInner;
  using type = T;
};

/// Return #nestedArray.flatten()[#index].
template &lt;auto nestedArray, std::size_t index,
          typename = std::enable_if_t&lt;NestedArray&lt;std::decay_t&lt;decltype(nestedArray)&gt;&gt;::value&gt;&gt;
consteval auto getValueByIndex() noexcept {
  using NestedArrayHelper = NestedArray&lt;std::decay_t&lt;decltype(nestedArray)&gt;&gt;;
  constexpr std::size_t innerIndex = index % NestedArrayHelper::inner;
  constexpr std::size_t outerIndex = index / NestedArrayHelper::inner;
  static_assert(NestedArrayHelper::inner &gt; innerIndex &amp;&amp;
                    NestedArrayHelper::outer &gt; outerIndex, "Index out of bounds.");
  return std::get&lt;innerIndex&gt;(std::get&lt;outerIndex&gt;(nestedArray));
}

/// Return 1-dimensional array #nestedArray.flatten() from 2-dimensional array #nestedArray.
/// @param nestedArray:   two-dimensional std::array (std::array&lt;std::array&lt;T, I&gt;, O&gt;)
/// @return               flat representation of #nestedArray (std::array&lt;T, I * O&gt;) with 
///                       nestedArray[o][i] = result[i + I * o]
template &lt;auto nestedArray&gt; 
requires requires() { requires NestedArray&lt;std::decay_t&lt;decltype(nestedArray)&gt;&gt;::value; }
consteval auto flattenNestedArray() noexcept {
  using NestedArrayHelper = NestedArray&lt;std::decay_t&lt;decltype(nestedArray)&gt;&gt;;
  using ContainedType = typename NestedArrayHelper::type;

  constexpr std::size_t flatSize = NestedArrayHelper::outer * NestedArrayHelper::inner;
  return []&lt;std::size_t... ix&gt;(std::index_sequence&lt;ix...&gt;) consteval noexcept {
    return std::array&lt;ContainedType, flatSize&gt; {getValueByIndex&lt;nestedArray, ix&gt;()...};
  }(std::make_index_sequence&lt;flatSize&gt;());
}


// Test, only for demonstrating the purpose of the code. 

/// 2d array 
constexpr std::array&lt;std::array&lt;int, 2&gt;, 3&gt; arrayOfPairs{{{1, 2}, {3, 4}, {15, 16}}};

/// Resulting 1d array
constexpr auto flatArray = flattenNestedArray&lt;arrayOfPairs&gt;();

// Test: Print out the resulting 1d array
int main () {
  for (std::size_t i = 0; i &lt; 6; ++i) { std::cout &lt;&lt; flatArray[i] &lt;&lt; " "; } std::cout &lt;&lt; std::endl;
}


I hope that this is appropriate for a Code Review, I linked the code in compilerExplorer. Note that it can only be compiled with gcc trunk, as clang trunk does not allow std::array as non-type template argument.
Thanks a lot :)
The biggest readability concern, in my opinion, is squashing everything on one line.  Compare:


int main () {
  for (std::size_t i = 0; i &lt; 6; ++i) { std::cout &lt;&lt; flatArray[i] &lt;&lt; " "; } std::cout &lt;&lt; std::endl;
}



to

int main()
{
    for (std::size_t i = 0; i &lt; 6; ++i) {
        std::cout &lt;&lt; flatArray[i] &lt;&lt; " ";
    }
    std::cout &lt;&lt; std::endl;
}


which can then be improved to:

int main()
{
    for (auto v : flatArray) {
        std::cout &lt;&lt; v &lt;&lt; ' ';
    }
    std::cout &lt;&lt; '\n';
}




We can use some shorthand to make nested arrays easier to understand:

namespace detail {
    template &lt;typename T, std::size_t... Dims&gt;
    struct multi_array;

    template &lt;typename T&gt;
    struct multi_array&lt;T&gt; {
        using type = T;
    };

    template &lt;typename T, std::size_t Dim, std::size_t... Dims&gt;
    struct multi_array&lt;T, Dim, Dims...&gt; {
        using type = std::array&lt;
            typename multi_array&lt;T, Dims...&gt;::type, Dim
        &gt;;
    };
}

template &lt;typename T, std::size_t... Dims&gt;
using multi_array = typename detail::multi_array&lt;T, Dims...&gt;::type;




Now, it is more idiomatic to pass the array as an argument instead of a template argument, so that invocation uses more natural syntax and works for both runtime values and compile time values:

template &lt;std::copy_­constructible T, std::size_t N, std::size_t M&gt;
constexpr auto flatten(const multi_array&lt;T, N, M&gt;&amp; array)
    noexcept(std::is_nothrow_copy_constructible_v&lt;T&gt;)
{
    return /* magic */;
}




We can also easily generalize into multiple dimensions from here:

#include &lt;array&gt;
#include &lt;concepts&gt;
#include &lt;type_traits&gt;

namespace detail {
    template &lt;typename T, std::size_t... Dims&gt;
    struct multi_array;

    template &lt;typename T&gt;
    struct multi_array&lt;T&gt; {
        using type = T;
    };

    template &lt;typename T, std::size_t Dim, std::size_t... Dims&gt;
    struct multi_array&lt;T, Dim, Dims...&gt; {
        using type = std::array&lt;
            typename multi_array&lt;T, Dims...&gt;::type, Dim
        &gt;;
    };

    template &lt;typename T&gt;
    struct multi_array_traits {
        static constexpr std::size_t size{1};
        using type = T;
    };

    template &lt;typename T, std::size_t N&gt;
    struct multi_array_traits&lt;std::array&lt;T, N&gt;&gt; {
        static constexpr std::size_t size{N * multi_array_traits&lt;T&gt;::size};
        using type = typename multi_array_traits&lt;T&gt;::type;
    };

    template &lt;std::size_t I&gt;
    constexpr const auto&amp; get(const auto&amp; scalar) noexcept
    {
        static_assert(I == 0);
        return scalar;
    }

    template &lt;std::size_t I, typename T, std::size_t N&gt;
    constexpr const auto&amp; get(const std::array&lt;T, N&gt;&amp; array) noexcept
    {
        constexpr auto InnerSize = multi_array_traits&lt;T&gt;::size;

        constexpr auto Outer = I / InnerSize;
        constexpr auto Inner = I % InnerSize;

        return detail::get&lt;Inner&gt;(array[Outer]);
    }

    template &lt;typename T, std::size_t... Indices&gt;
    constexpr auto flatten(const T&amp; array, std::index_sequence&lt;Indices...&gt;)
    {
        constexpr auto Size = multi_array_traits&lt;T&gt;::size;
        using Elem = typename multi_array_traits&lt;T&gt;::type;

        return std::array&lt;Elem, Size&gt;{Elem(detail::get&lt;Indices&gt;(array))...};
    }
}

template &lt;typename T, std::size_t... Dims&gt;
using multi_array = typename detail::multi_array&lt;T, Dims...&gt;::type;

template &lt;std::copy_constructible T&gt;
constexpr auto flatten(const T&amp; array)
{
    constexpr auto Size = detail::multi_array_traits&lt;T&gt;::size;
    return detail::flatten(array, std::make_index_sequence&lt;Size&gt;{});
}


(live demo)