用嵌套的initializer_list初始化多维数组

6
出于某些原因,我必须在C++中实现一个多维数组类。 该数组的结构如下:
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是存储在数组中的元素类型,而数组的维度不是模板化的,因为用户应该能够重新调整数组的形状,例如:

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

虽然上述功能的实现进展顺利,但我在实现这个类的“开始”时遇到了困难,即初始化数组……我的问题如下:

  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?


1
C++11中是否有一种方法可以传递嵌套的初始化列表来构造2D矩阵?这个问题有帮助吗? - user4581301
@user4581301 我看过那个问题,但我不认为它对我的当前情况有帮助。在那个问题中,矩阵始终是二维的,但对于我的数组来说并非如此,我不能只写尽可能多的嵌套std::initializer_list...另外,那个问题也无法回答我的第二个问题。 - Chlorie
2个回答

2
唯一可能的解决方案,只涉及initializer_list,就是声明与可能维度数量相等的构造函数:
template<class T>
Array(std::initializer_list<T>)

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

...

原文如下:

其原因在[temp.deduc.call]/1中给出:(P为模板参数)

如果从P中移除引用和cv限定符可得到std::initializer_list[...],并且参数是非空的初始化列表([dcl.init.list]),则对于初始化列表的每个元素执行推导,将P'作为函数模板参数类型,将初始化元素作为其参数[...] 否则,初始化列表参数会导致该参数被视为无法推导的上下文。

因此,如果函数参数是std::initializer_list<T>,则初始化列表参数的嵌套元素本身不能是初始化列表。

如果您不想声明那么多构造函数,另一个选项是明确指定参数的类型为std::initializer_list,以避免模板参数推导。 下面我使用了一个名为“nest”的类,只是因为它的名称更短:

#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}}};
  }

1
@PiotrSkotnicki 我知道!nest旨在用作初始化列表,仅用于初始化对象。x的构造函数不应存储initializer_list。 - Oliv
谢谢你的回答!顺便问一下,你说这是“唯一可能涉及initializer_list的解决方案”,如果问题不限于initializer_list怎么办? - Chlorie
@Chlorie 我曾经考虑过一种涉及聚合初始化和花括号省略的技巧,但我错了。另一方面,我无法进行逻辑证明,证明没有其他解决方案。现在我严重怀疑是否有任何其他解决方案。 - Oliv
我并不完全理解[temp.deduc.call]/1是问题的原因。如果那是唯一的限制,template<typename T> struct Array { template<typename U> Array(std::initializer_list<U> {...} } 理论上应该允许你执行 Array<int> arr{{1, 2}, {3, 4}},但实际上却不行。 - QuaternionsRock

1
我可能有点晚了,但是不需要多个构造函数也可以做到,下面是从初始化列表中提取数据的源代码,有点 hacky。整个技巧在于构造函数会隐式地使用正确的类型进行调用。
#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";
}

输出

 2 3
 1 2 3 92 1 3

shape()函数将返回每个维度张量的形状。数组会按照其写入的方式精确保存。创建类似于shape这样的东西非常重要,因为它将给出元素的排序方式。
如果您想从张量中获取特定索引,比如a[1][2][3],正确的位置在1*a.shape[1]+a.shape[2]+2*a.shape[2]+3。
如果您不想创建张量或多维数组,我建议将所有内容存储为列表,1D数组中的条目引用是非常复杂的。这段代码仍然是一个很好的起点。一些细节和技巧可以在以下链接中找到: https://github.com/martinpflaum/multidimensional_array_cpp

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接