Multi-dimension array template with dimension deduction

403 views Asked by At

I want to make an NDArray template which has a fixed dimension, but can be resized across each dimension.

My question is how to make it be able to deduce the dimensions in the constructor according to how many pair of {} is used? The elements in the constructor will be used to initialize some of the elements.

#include <array>
#include <iostream>

template<typename T, size_t Dimension>
class NDArray
{
    T* buffer = nullptr; //flattened buffer for cache locality
    std::array<size_t, Dimension> dimension;    //keep the current sizes of each dimension
public:
    NDArray(std::initializer_list<T> elements) : dimension{elements.size()}   //for 1D
    {
        std::cout << "Dimension = " << Dimension << '\n';
    }
    NDArray(std::initializer_list<NDArray<T, Dimension-1>> list) //how to make this works???
    {
        std::cout << "Dimension = " << Dimension << '\n';
    }
};

template<typename T, size_t N>
NDArray(const T(&)[N]) -> NDArray<T, 1>;

int main()
{
    NDArray a{ {3,4,5} };//OK, NDArray<int, 1> because of the deduction guide
    NDArray b{ {{1,2,3}, {4,5,6}} };//Nope, I want: NDArray<int, 2>
}
2

There are 2 answers

2
Barry On

This is impossible in the general case, but possible in however many specific cases you want to spell out.

An initializer list has no type. The only way you can deduce a type for it (as in, separate from having a default template argument) is that we have two special cases spelled out in [temp.deduct.call]/1:

If removing references and cv-qualifiers from P gives std::initializer_list<P′> or P′[N] for some P′ and N and the argument is a non-empty initializer list ([dcl.init.list]), then deduction is performed instead for each element of the initializer list independently, taking P′ as separate function template parameter types P′i and the ith initializer element as the corresponding argument. In the P′[N] case, if N is a non-type template parameter, N is deduced from the length of the initializer list. Otherwise, an initializer list argument causes the parameter to be considered a non-deduced context ([temp.deduct.type]).

This is the rule that lets the following work:

template <typename T>
constexpr auto f(std::initializer_list<T>) -> int { return 1; }

static_assert(f({1, 2, 3}) == 1);

But that isn't enough to get this to work:

static_assert(f({{1, 2}, {3, 4}}) == 1); // ill-formed (no matching call to f)

Because the rule is - okay, we can strip one layer of initializer_list but then we have to deduce the elements. And once we strip one layer of initializer list, we're trying to deduce T from {1, 2} and that fails - we can't do that.

But we know how to deduce something from {1, 2} - that's this same rule. We just have to do it again:

template <typename T>
constexpr auto f(std::initializer_list<T>) -> int { return 1; }

template <typename T>
constexpr auto f(std::initializer_list<std::initializer_list<T>>) { return 2; }


static_assert(f({1, 2, 3}) == 1);
static_assert(f({{1, 2}, {3, 4}}) == 2);

and again:

template <typename T>
constexpr auto f(std::initializer_list<T>) -> int { return 1; }

template <typename T>
constexpr auto f(std::initializer_list<std::initializer_list<T>>) { return 2; }

template <typename T>
constexpr auto f(std::initializer_list<std::initializer_list<std::initializer_list<T>>>) { return 3; }


static_assert(f({1, 2, 3}) == 1);
static_assert(f({{1, 2}, {3, 4}}) == 2);
static_assert(f({{{1, 2}, {3, 4}}, {{5, 6}, {7, 8}}}) == 3);

The same way we have the carve-out for std::initializer_list<T>, we also have the carve-out for T[N]. That works the same way, just a bit less typing:

template <typename T, size_t N>
constexpr auto f(T(&&)[N]) -> int { return 1; }

template <typename T, size_t N1, size_t N2>
constexpr auto f(T(&&)[N1][N2]) { return 2; }

template <typename T, size_t N1, size_t N2, size_t N3>
constexpr auto f(T(&&)[N1][N2][N3]) { return 3; }


static_assert(f({1, 2, 3}) == 1);
static_assert(f({{1, 2}, {3, 4}}) == 2);
static_assert(f({{{1, 2}, {3, 4}}, {{5, 6}, {7, 8}}}) == 3);
0
Amir Kirsh On

You may achieve almost what you want, if you are OK with explicitly creating NDArray inside the std::initialize_list this way:

int main()
{
    NDArray a{3,4,5}; // would be deduced to NDArray<int, 1>
    NDArray b{ NDArray{1,2,3}, {4,5,6} }; // would be deduced to NDArray<int, 2>
}

Note that it is enough to explicitly add NDArray only for the first appearance of each dimension. For example, for 3D NDArray:

NDArray c { NDArray{ NDArray{8, 3}, {1, 2}, {1, 2, 3} }, 
                   {        {4, 5}, {8, 9, 7}, {2, 5} } };

To achieve that you need to have these two deduction guides:

template<typename T>
NDArray(const std::initializer_list<T>&)
                -> NDArray<T, 1>;

template<typename T, size_t DIMENSIONS>
NDArray(const std::initializer_list<NDArray<T, DIMENSIONS>>&) 
                -> NDArray<T, DIMENSIONS + 1>;

Code example: http://coliru.stacked-crooked.com/a/1a96b1eaa0717a67