In practice, you can treat a N-dimensional array allocated like that as a one-dimensional array. While the standard will tell you not to, there is far far far too much code in production that does this for it to ever not be supported in an actual shipping commercial general purpose compiler.
At some point the wording of the standard will be modified to permit it, I'd be willing to bet good money on that, if it already hasn't been slipped in. But the last time I looked (and it was a while ago) it was still undefined behavior.
You do have to be careful and ensure you don't have a "jagged" array, ie an array of pointers to arrays. But your example is not an example of that.
Due to my dislike of using UB, I personally end up writing code to manage a flat buffer and provide a n-dimensional view of it that works just like it was a tiered buffer. As a toy 2 dimensional example:
template<class T>
struct as_const {
using type=T const;
};
template<class T>
using as_const_t = typename as_const<T>::type;
template<class T, std::size_t N, class Ref=T&, std::size_t stride=1>
class view_helper {
public:
T* buffer = nullptr;
template<class, std::size_t, class, std::size_t>
friend class view_helper;
protected:
explicit view_helper(T& r):view_helper(std::addressof(r)) {}
public:
view_helper(T* p):buffer(p){}
view_helper(view_helper const&)=default;
view_helper& operator=(view_helper const&)=delete;
Ref operator[](std::size_t n) {
return Ref(buffer[stride*n]);
}
as_const_t<Ref> operator[](std::size_t n) const {
return as_const_t<Ref>(buffer[stride*n]);
}
T* data() { return buffer; }
T const* data() const { return buffer; }
std::size_t size() const { return N; }
};
template<class T>
struct as_const<T&> {
using type=T const&;
};
template<class T, std::size_t N, class R, std::size_t S>
struct as_const<view_helper<T,N,R,S>> {
using type=view_helper<const T,N, as_const_t<R>,S>;
};
template<class T, std::size_t...Dims>
struct view;
template<class T, std::size_t...Dims>
using view_t = typename view<T,Dims...>::type;
template<class T, std::size_t X>
struct view<T, X> {
using type = view_helper<T, X>;
};
template<class T, std::size_t X0, std::size_t...Xs>
struct view<T, X0, Xs...> {
using type = view_helper< T, X0, view_t<T, Xs...>, (1*...*Xs)>;
};
now we have
int raw_buffer[24];
view<int,6,2,2> bob(raw_buffer);
is a 3 dimensional view of a 24 element array of integers as if it was a int[6][2][2].
Implementing iterators takes just a bit of work on top of this; the easy way is to just write an indexed-range-to-iteration thing that keeps track of a pointer to the range and a std::size_t index, then invokes (*range)[i] when you dereference.
Live example.
std::arrayorstd::vector?std::array<int, X*Y> flat; auto f2d=flat | std::views::chunk(Y); f2d[i][j];