Nested initializer_list for initializing multidimensional arrays
Asked Answered
U

2

6

For some reasons I have to implement a multidimensional array class in C++. The array in question is something like this:

template <typename T, typename = std::enable_if_t<std::is_arithmetic_v<T>>>
class Array final
{
private:
    std::vector<size_t> shape_;
    std::vector<T> data_;
public:
    // Some public methods
}

T is the type of elements stored in the array, and that the dimensions of the array is not templated since the user should be able to reshape the array, for example:

Array<int> array = Array<int>::zeros(3, 2, 4);
array.reshape(4, 6);

Though implementation of the functions mentioned above went quite smoothly, I stuck at the "beginning" of implementing this class, that is to initialize the array... My questions are as follows:

  1. Is there any method to have an constructor like this, such that nested initializer lists of different depths create different arrays, like:

    Array<int> a = {1, 2}; // Dimension: 1, Shape: 2
    Array<int> b = {{1}, {2}}; // Dimension: 2, Shape: 2x1
    

    My approach to implement the constructors made these two arrays the same, which is not what I want. Plus, clang complained about the braced scalars, which seem to be the problem. Currently my naive approach which suffers from the problem above looks like this

    ...
        Array() :data_(0), shape_{0} {}
        Array(std::initializer_list<T> list) :data_(list), shape_{list.size()} {}
        Array(std::initializer_list<Array> lists)
        {
            // Implementation
        }
    ...
    
  2. It's easy for the compiler to deduct the types for the following arrays:

    Array c = {1, 2}; // T = int
    Array d = {1.0, 2.0}; // T = double
    

    But I failed to write a working deduction guide for multidimensional ones:

    Array e = {{1, 2}, {3, 4}}; // Expects T = int
    Array f = {{1.0, 2.0}, {3.0, 4.0}}; // Expects T = double
    

    Is there any way to write a type deduction guide for this class?

Unavailing answered 20/11, 2018 at 5:28 Comment(2)
Does is there a way to pass nested initializer lists in C++11 to construct a 2D matrix? help?Bastia
@Bastia I've seen that question and I don't think that helps with my current situation. In that question the matrix is always 2D, but it's not the case for my Array, and I can't just write as many nested std::initializer_list as I can... Plus that question doesn't help with my second question either.Unavailing
W
2

The only possible solution that would only involve initializer_list would be to declare a number of constructors that equals the number of possible dimensions:

template<class T>
Array(std::initializer_list<T>)

template<class T>
Array(std::initializer_list<std::initializer_list<T>>)

...

The reason is given in [temp.deduc.call]/1: (P the template parameter)

If removing references and cv-qualifiers from P gives std::initializer_­list [...] and the argument is a non-empty initializer list ([dcl.init.list]), then deduction is performed instead for each element of the initializer list, taking P' as a function template parameter type and the initializer element as its argument [...] Otherwise, an initializer list argument causes the parameter to be considered a non-deduced context

So if the function parameter is std::initializer_list<T> the nested element of the initializer list argument can not be itself an initializer list.

If you don't want to declare that many constructors, the other option is to explictly specify that the argument is of type std::initializer_list in order to avoid template argument deduction. Below I use a class named "nest" just because its name is shorter:

#include<initializer_list>

using namespace std;

template<class T>
struct nest{
  initializer_list<T> value; 
  nest(initializer_list<T> value):value(value){}
  };
template<class T>
nest(initializer_list<T>)->nest<T>;

struct x{
   template<class T>
   x(initializer_list<T>);
   };

int main(){
  x a{1,2,3,4};
  x b{nest{1,2},nest{3,4}};
  x c{nest{nest{1},nest{2}},nest{nest{3},nest{4}}};
  }
Wellington answered 20/11, 2018 at 11:21 Comment(4)
@PiotrSkotnicki I do know! nest is intended to be used as initializer list, that is just used to initialize an object. The constructor of x is not supposed to store the initializer_list.Wellington
Thanks for your answer! By the way you said that this is "The only possible solution that would only involve initializer_list", what if the question is not restricted to initializer_list?Unavailing
@Unavailing I had thought about a trick involving aggregates initialization and brace elision but I was erroneous. On the other I can't make a logical proof there are no other solutions. Now I seriously doubt there is any.Wellington
I don't fully understand [temp.deduc.call]/1 is the cause of the issue. If that were the only restriction, template<typename T> struct Array { template<typename U> Array(std::initializer_list<U> {...} } should theoretically allow you to do Array<int> arr{{1, 2}, {3, 4}}, which you can't.Abseil
S
2

i might be a bit late but its 100% possible without multiple constructors below is the source code to extract the data from the initilizer_list its a bit hacky. The whole trick is that the constructor are implicitly called with the correct type.

#include <initializer_list>
#include <iostream>
using namespace std;

class ShapeElem{
public:
    ShapeElem* next;
    int len;

    ShapeElem(int _len,ShapeElem* _next): next(_next),len(_len){}

    void print_shape(){
        if (next != nullptr){
            cout <<" "<< len;
            next->print_shape();
        }else{
            cout << " " <<  len << "\n";
        }
    }

    int array_len(){
        if (next != nullptr){
            return len*next->array_len();
        }else{
            return len;
        } 
    }
};

template<class value_type>
class ArrayInit{
public:
    void* data = nullptr;
    size_t len;
    bool is_final;

    ArrayInit(std::initializer_list<value_type> init) : data((void*)init.begin()), len(init.size()),is_final(true){}

    ArrayInit(std::initializer_list<ArrayInit<value_type>> init): data((void*)init.begin()), len(init.size()),is_final(false){}

    ShapeElem* shape(){
        if(is_final){
            ShapeElem* out = new ShapeElem(len,nullptr);
        }else{
            ArrayInit<value_type>* first = (ArrayInit<value_type>*)data;
            ShapeElem* out = new ShapeElem(len,first->shape());
        }
    }
    void assign(value_type** pointer){
        if(is_final){
            for(size_t k = 0; k < len;k ++ ){
                (*pointer)[k] =  ( ((value_type*)data)[k]);
            }
            (*pointer) = (*pointer) + len;
        }else{
            ArrayInit<value_type>* data_array = (ArrayInit<value_type>*)data;
            for(int k = 0;k < len;k++){
                data_array[k].assign(pointer);
            }
        }
    }
};


int main(){
    auto x = ArrayInit<int>({{1,2,3},{92,1,3}});
    auto shape = x.shape();
    shape->print_shape();
    int* data = new int[shape->array_len()];
    int* running_pointer = data;
    x.assign(&running_pointer);
    for(int i = 0;i < shape->array_len();i++){
        cout << " " << data[i];
    }
    cout << "\n";
}

outputs

 2 3
 1 2 3 92 1 3

The shape() function will return you the shape of the tensor at each dimension. The array is exactly saved as it is written down. It's really import to create something like shape since this will give you the ordering in which the elements are.

If you want a specific index out of the tensor lets say a[1][2][3] the correct position is in 1*a.shape[1]a.shape[2] + 2a.shape[2] + 3

if you are not looking to create tensors or multidimensional arrays i would recommand storing everything as a list,the referencing of entries in a 1D array is then really complicated. This code should then still be a good starting point. Some minor details and tricks can be found in: https://github.com/martinpflaum/multidimensional_array_cpp

Salinger answered 22/7, 2021 at 9:58 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.