Home > Enterprise >  Is it possible to use variadic template parameters to initialise multi dimensional containers?
Is it possible to use variadic template parameters to initialise multi dimensional containers?

Time:11-16

I am trying to use variadic template and constructor arguments to initialise values of a multi dimensional arrays inside a custom class ArrayND. Up until this point I've been successful initialising arbitrary dimensional Array instances with multi dimensional std::arrays, but the double bracing that's mostly but not always needed for std::array initialisation gets a bit ugly and hard to parse, for example:

constinit ArrayND<5, 4, 6> my_nd_array({
    {
        {{ {0.4f}, {0.6f}, {0.1f}, {0.4f} }},
        {0.4f, 0.6f, 0.1f, 0.4f},
        {0.4f, 0.6f, 0.1f, 0.4f},
        {0.4f, 0.6f, 0.1f, 0.4f},
        {0.4f, 0.6f, 0.1f, 0.4f},
    }
});

So far, I can initialise a 1D implementation of the Array class without trouble, although I'd rather require a single set of curly braces around the values in this instance:

template<size_t SIZE>
struct Array1D {

    template<class ... VALUES>
    constexpr explicit Array1D(const VALUES ... values)
            : data({values...})
    {}

    std::array<float, SIZE> data;
};

constinit Array1D<2> my_2_array(
        0.1f, 0.2f
);

But I'm not sure if/how it's possible to do a similar thing to cleanly initialise higher dimensional arrays. I've been experimenting with the Array2D, but neither version of the initialisation I've pasted below works:

template<size_t SIZE_0, size_t SIZE_1>
struct Array2D {

    template<class ... VALUES>
    constexpr explicit Array2D(const VALUES ... values)
            : data(values...)
    {}

    std::array<std::array<float, SIZE_1>, SIZE_0> data;
};

// ERROR: No matching constructor...
constinit Array2D<2, 2> my_2x2_array_A(
        { 0.1f, 0.2f }, { 0.3f, 0.4f }
);

// ERROR: No matching constructor...
constinit Array2D<2, 2> my_2x2_array_B(
        { { 0.1f, 0.2f }, { 0.3f, 0.4f } }
);

However, the eventual goal is to be able to do the same thing with my ArrayND currently which looks like this:

template<size_t SIZE, size_t ... SUB_SHAPE>
struct ArrayND {

    template<class ... VALUES>
    constexpr explicit ArrayND(const VALUES ... values)
            : data(values...)
    {}

    using DataType = std::array<typename ArrayND<SUB_SHAPE...>::DataType, SIZE>;
    DataType data;
};

Anyone got any ideas on how to achieve something like this without passing in a multi dimensional std::array?

To clarify, the constexpr constructor is important to what I need to do. I have a std::vector version that works just fine, but std::vector still has no consexpr constructor in clang 13.0 (and I'm not sure it's even accepted into the standard anyway, it's possible only MSVC implemented this feature...).

EDIT:

It's possibly worth noting that in the non-cutdown implementations, these arrays will all derive from the final ND class, with specialisations for scalar and 1D versions. The Array1D and Array2D classes are just for testing solutions to this issue in a simpler form.

UPDATE:

I'd rather not end up managing and writing algorithms for C arrays if I can avoid it, but it occurred to me I hadn't tried at least using them for construction... But I I'm struggling to get that working as well:

template<size_t SIZE_0, size_t SIZE_1>
struct Array2D {

    constexpr explicit Array2D(const float values[SIZE_0][SIZE_1])
            : data(values)
    {}

    float data[SIZE_0][SIZE_1];
};

// ERROR: No matching constructor...
constinit Array2D<2, 2> my_2x2_array_0(
        {{ 0.1f, 0.2f }, { 0.3f, 0.4f }}
);

// ERROR: No viable conversion from 'float[2][2]' to 'Array2D<2, 2>'. Explicit constructor is not a candidate
// Personal note: It appears to be trying to use the copy constructor, which I guess kind of makes sense
constinit float raw_data[2][2] = {{ 0.1f, 0.2f }, { 0.3f, 0.4f }};
constinit Array2D<2, 2> my_2x2_array_1(raw_data);

CodePudding user response:

1. Using aggregate initialization (easy)

If you just leave out your constructor and make the data field public you can take advantage of aggregate initialization:


template<std::size_t SIZE_0, std::size_t SIZE_1, std::size_t SIZE_2>
struct Array3D {
    std::array<std::array<std::array<float, SIZE_2>, SIZE_0>, SIZE_0> data;
};

constinit Array3D<2,2,2> arr{
    {{
            {{
                {1, 2},
                {3, 4},
            }},
            {{
                {5, 6},
                {7, 8},
            }}
    }}
};

(godbolt example)

This also allows type-conversion (using ints in the example), however it'll take quite a few extra curly-braces.

2. passing in a multidimensional std::array (easy)

template<std::size_t SIZE_0, std::size_t SIZE_1, std::size_t SIZE_2>
struct Array3D {
    constexpr Array3D(std::array<std::array<std::array<float, SIZE_2>, SIZE_0>, SIZE_0> arr)
        : data(arr) {}

    std::array<std::array<std::array<float, SIZE_2>, SIZE_0>, SIZE_0> data;
};

constinit Array3D<2,2,2> arr{
    {{
            {{
                {1, 2},
                {3, 4},
            }},
            {{
                {5, 6},
                {7, 8},
            }}
    }}
};

(godbolt example)

This basically behaves the same way as 1., with the additional benefit that you can make data private.

3. Use a c array instead of std::array (easy)

By using a c array you can use aggregate initialization like you want without double braces:

template<size_t SIZE_0, size_t SIZE_1, size_t SIZE_2>
struct Array3D {
    float data[SIZE_0][SIZE_1][SIZE_2];
};

constinit Array3D<2,2,2> arr {
   {
       {
           {1, 2},
           {3, 4}
       },
       {
           {5, 6},
           {7, 8}
       }
   }
};

(godbolt example)

4. Pass a c array as constructor parameter

By passing a c array you get the same effect as in 3., with the added benefit of being able to keep the std::array's

template<std::size_t SIZE_0, std::size_t SIZE_1, std::size_t SIZE_2>
struct Array3D {
    using array_type = float[SIZE_0][SIZE_1][SIZE_2];

    constexpr Array3D(array_type const& values) {
        for(int i = 0; i < SIZE_0; i  ) {
            for(int j = 0; j < SIZE_1; j  ) {
                for(int k = 0; k < SIZE_2; k  ) {
                    data[i][j][k] = values[i][j][k];
                }
            }
        }
    }

    std::array<std::array<std::array<float, SIZE_0>, SIZE_1>, SIZE_2> data;
};

constinit Array3D<2,2,2> arr {
   {
       {
           {1, 2},
           {3, 4}
       },
       {
           {5, 6},
           {7, 8}
       }
   }
};

(godbolt example)

5. Using std::initializer_list (hard)

With std::initializer_list you get the kind of initialization you want.
However you'll have to do quite a lot of work that the compiler would do for you in 1. / 2., e.g. verifying the sizes of the lists and filling missing numbers with 0.


// Helper to get the array type for an n-dimensional array
// e.g.: n_dimensional_array_t<float, 2, 2> == std::array<std::array<float, 2>, 2>

template<class T, std::size_t... dimensions>
struct n_dimensional_array;

template<class T, std::size_t... dimensions>
using n_dimensional_array_t = typename n_dimensional_array<T, dimensions...>::type;

template<class T, std::size_t last>
struct n_dimensional_array<T, last> {
    using type = std::array<T, last>;
};

template<class T, std::size_t first, std::size_t... others>
struct n_dimensional_array<T, first, others...> {
    using type = std::array<n_dimensional_array_t<T, others...>, first>;
};

// Helper to construct nested initializer_lists
// e.g.: nested_initializer_list_t<int, 2> == std::initializer_list<std::initializer_list<int>>

template<class T, std::size_t levels>
struct nested_initializer_list {
    using type = std::initializer_list<typename nested_initializer_list<T, levels - 1>::type>;
};

template<class T>
struct nested_initializer_list<T, 0> {
    using type = T;
};

template<class T, std::size_t levels>
using nested_initializer_list_t = typename nested_initializer_list<T, levels>::type;


// Helper to recursively fill a n-dimensional std::array
// with a nested initializer list.

template<class T, std::size_t... dimensions>
struct NestedInitializerListHelper;
    
template<class T, std::size_t last>
struct NestedInitializerListHelper<T, last> {
    static constexpr void initialize(
        std::array<T, last>& arr,
        std::initializer_list<T> init
    ) {
        if(init.size() > last)
            throw std::invalid_argument("Too many initializers for array!");
        
        for(int i = 0; i < last; i  ) {
            if(i < init.size())
                arr[i] = std::data(init)[i];
            else
                arr[i] = {};
        }
    }
};

template<class T, std::size_t first, std::size_t... other>
struct NestedInitializerListHelper<T, first, other...> {
    static constexpr void initialize(
        n_dimensional_array_t<T, first, other...>& arr,
        nested_initializer_list_t<T, 1   sizeof...(other)> init
    ) {
        if(init.size() > first)
            throw std::invalid_argument("Too many initializers for array!");

        for(int i = 0; i < first; i  ) {
            if(i < init.size())
                NestedInitializerListHelper<T, other...>::initialize(
                    arr[i],
                    std::data(init)[i]
                );
            else
                NestedInitializerListHelper<T, other...>::initialize(
                    arr[i],
                    {}
                );
        }
    }
};


template<class T, std::size_t... dimensions>
struct MultidimensionalArray {
    using array_type = n_dimensional_array_t<T, dimensions...>;
    using initializer_type = nested_initializer_list_t<T, sizeof...(dimensions)>;

    // Initializer-list constructor for nice brace-initialization
    constexpr explicit MultidimensionalArray(initializer_type init) {
        using init_helper = NestedInitializerListHelper<T, dimensions...>;
        init_helper::initialize(data, init);
    }

    // (optional)
    // array-based constructor to allow slicing of Multidimensional arrays,
    // and construction from a list of numbers
    // e.g.:
    //   // slicing
    //   MultidimensionalArray<int, 2, 2> k;
    //   MultidimensionalArray<int, 2> another(k[0]); 
    //
    //   // initialization from flat array / list of values
    //   MultidimensionalArray<int, 2, 2> j {{1,2,3,4}};
    constexpr explicit MultidimensionalArray(array_type arr): data(arr) {
    }

    // (optional)
    // array indexing operator
    constexpr auto& operator[](std::size_t i) {
        return data[i];
    }


    // (optional)
    // array indexing operator
    constexpr auto const& operator[](std::size_t i) const {
        return data[i];
    }

    // (optional)
    // allow conversion to array type (for slicing)
    explicit constexpr operator array_type() {
        return data;
    }

private:
    array_type data;
};

constinit MultidimensionalArray<float, 2, 2, 2> arr{
   {
       {
           {1, 2},
           {3, 4}
       },
       {
           {5, 6},
           {7, 8}
       }
   }
};

I've also combined the 1D / 2D / nD cases into a single class for less boilerplate.

This behaves just as one would expect it, you can leave out any part and that one would be initialized with 0, e.g.:

constinit MultidimensionalArray<float, 2, 2, 2> arr{
    {
        {},
        {
            {1, 2}
        }
    }
};

// inits arr with:
// 0 0
// 0 0
// 1 2
// 0 0

There are also a few optional methods, that add a few convenience features like slicing support and indexing operators.

If you want you can test in on godbolt.

CodePudding user response:

std::array indeed requires double brace as it uses aggregate initialization.

You might create your version which has a constructor taking N parameters instead:

// helper used for variadic expension
template <std::size_t, typename T> using always_t = T;

template <typename T, typename Seq> struct my_array_impl;

template <typename T, std::size_t... Is>
struct my_array_impl<T, std::index_sequence<Is...>>
{
    // Here, constructor is not template <typename ... Ts>
    // {..} has no type, and can only be deduced
    // to some types we are not interested in
    // We use always_t<Is, T> trick to have constructor similar to
    // my_array_impl(T arg1, T arg2, .., T argN)
    constexpr my_array_impl(always_t<Is, T>... args) : data{{args...}} {}

    // ...

    std::array<T, sizeof...(Is)> data;
};

template <typename T, std::size_t N>
using my_array = my_array_impl<T, std::make_index_sequence<N>>;

and then

// just compute type alias currently
// to have my_array<my_array<..>, Size>
template <typename T, std::size_t I, std::size_t ... Is>
struct ArrayND_impl<T, I, Is...>
{
    using DataType = my_array<typename ArrayND_impl<T, Is...>::DataType, I>;
};

template <typename T, std::size_t ... Sizes>
using ArrayND = typename ArrayND_impl<T, Sizes...>::DataType;

With usage similar to:

template <std::size_t SIZE_0, std::size_t SIZE_1, std::size_t SIZE_2>
using Array3D = ArrayND<float, SIZE_0, SIZE_1, SIZE_2>;

constinit Array3D<2,2,2> arr(
    {
        {1, 2},
        {3, 4},
    },
    {
        {5, 6},
        {7, 8},
    }
);

Demo.

  • Related