【问题标题】:Is it possible to use variadic template parameters to initialise multi dimensional containers?是否可以使用可变参数模板参数来初始化多维容器?
【发布时间】:2021-12-26 21:43:07
【问题描述】:

我正在尝试使用可变参数模板和构造函数参数来初始化自定义类 ArrayND 内的多维数组的值。到目前为止,我已经成功地用多维 std::arrays 初始化任意维 Array 实例,但是 std::array 初始化主要但并不总是需要的双支撑变得有点丑陋且难以解析,例如:

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},
    }
});

到目前为止,我可以毫无问题地初始化 Array 类的一维实现,尽管我宁愿在此实例中的值周围使用一组花括号:

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
);

但我不确定是否/如何做类似的事情来干净地初始化高维数组。我一直在尝试使用 Array2D,但我在下面粘贴的初始化版本都不起作用:

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

然而,最终的目标是能够用我的 ArrayND 做同样的事情,目前看起来像这样:

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

有人知道如何在不传入多维std::array的情况下实现这样的目标吗?

为了澄清,constexpr 构造函数对我需要做的事情很重要。我有一个工作正常的std::vector 版本,但std::vector 在clang 13.0 中仍然没有consexpr 构造函数(而且我不确定它是否被标准接受,可能只有MSVC 实现了这个功能。 .).

编辑:

可能值得注意的是,在非缩减实现中,这些数组都将派生自最终的 ND 类,并专门针对标量和一维版本。 Array1D 和 Array2D 类仅用于以更简单的形式测试此问题的解决方案。

更新:

如果可以避免的话,我宁愿不为 C 数组管理和编写算法,但我突然想到,我至少没有尝试过将它们用于构造......但我很难得到也可以:

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);

【问题讨论】:

  • FWIW,如果您切换到使用一维数组来存储任何 Nd 数组,那么初始化变得微不足道。这也为您提供了最大的空间效率和您可以获得的缓存友好性。
  • 我已经考虑过了,但是除了我对这个想法的其他抱怨之外,这并不能帮助我获得视觉上不错的 ND 数组初始化
  • 理想情况下,我希望我的初始化在 n 维数组结构中每个数组正好有 1 个花括号
  • 为什么不简单地将 Array2D 设为 Array1D 的 Array1D 的 typdef。与 ArrayND 相同
  • 你的意思是完全去掉std::array

标签: c++ templates variadic-templates c++20 class-template


【解决方案1】:

1。使用聚合初始化(简单)

如果您只是省略构造函数并将data 字段公开,则可以利用聚合初始化:


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)

这也允许类型转换(在示例中使用整数),但是它需要很多额外的花括号。

2。传入多维std::array(简单)

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)

这基本上与 1. 的行为方式相同,但您可以将 data 设为私有。

3。使用 c 数组而不是 std::array(简单)

通过使用 c 数组,您可以使用不需要双括号的聚合初始化:

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。传递一个 c 数组作为构造函数参数

通过传递一个 c 数组,您可以获得与 3. 中相同的效果,另外还有一个好处是能够保留 std::array

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。使用std::initializer_list(硬)

使用std::initializer_list,您可以获得所需的初始化类型。
但是,您必须完成编译器在 1. / 2. 中为您完成的大量工作,例如验证列表的大小并用 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}
       }
   }
};

我还将 1D / 2D / nD 案例合并到一个类中以减少样板。

这与预期的一样,您可以省略任何部分,该部分将被初始化为 0,例如:

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

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

还有一些可选方法,它们添加了一些便利功能,例如切片支持和索引运算符。

如果你愿意,可以test in on godbolt

【讨论】:

  • 刚刚通过解决方案 3。我做了一个非常小的编辑(在 2 个地方添加类型名)以使其也兼容 clang。我会直截了当地说我正在与std::initializer_list 斗争,但如果那是一个观众,我会再试一次。解决方案 1 和 2 是迄今为止我对我的主 ND 类所做的事情,并且需要我试图摆脱的双/不一致大括号初始化
  • @IronAttorney 我添加了更多选项,也许这些更适合您的问题。
  • 感谢您的帮助!我确实从您在此处发布的内容中获得了一些灵感,并且我发布了一个显示当前工作解决方案的答案
【解决方案2】:

std::array 确实需要双括号,因为它使用聚合初始化。

你可以创建你的版本,它有一个带有 N 个参数的构造函数:

// 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>>;

然后

// 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;

用法类似于:

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.

【讨论】:

  • 这看起来非常干净,而且您还没有使用std::initialiser_list 或进行任何循环来初始化数据数组。我明天一定会来看看这个!与此同时,我得到了一个与std::initialiser_list 合作的解决方案,我认为我会发布。我不喜欢我正在写出循环来初始化成员变量的事实,但它至少让我现在有了一些具有所需初始化语法的工作对象
【解决方案3】:

感谢@Turtlefight 和@Jarod42 的回答。我还没有太多时间看你的@Jarod42,但我会的。自从@Turtlefight 发布以来,我一直在研究我的解决方案,我认为在采纳建议后不分享我当前的工作解决方案是很愚蠢的。

Godbolt link here!

这是定义类和辅助函数的代码:

template<class VALUE_TYPE, uint32_t SIZE, uint32_t ... SUB_SHAPE>
struct ArrayND;

template<class VALUE_TYPE, uint32_t SIZE>
constexpr auto init(const typename ArrayND<VALUE_TYPE, SIZE>::InitType& initializer_list) -> typename ArrayND<VALUE_TYPE, SIZE>::DataType
{
    typename ArrayND<VALUE_TYPE, SIZE>::DataType array;
    for (int i = 0; i < initializer_list.size(); i++) {
        array[i] = std::data(initializer_list)[i];
    }
    return array;
}

template<class VALUE_TYPE, uint32_t SIZE, uint32_t SUB_SIZE, uint32_t ... SUB_SUB_SHAPE>
constexpr auto init(const typename ArrayND<VALUE_TYPE, SIZE, SUB_SIZE, SUB_SUB_SHAPE...>::InitType& initializer_list) -> typename ArrayND<VALUE_TYPE, SIZE, SUB_SIZE, SUB_SUB_SHAPE...>::DataType
{
    typename ArrayND<VALUE_TYPE, SIZE, SUB_SIZE, SUB_SUB_SHAPE...>::DataType array;
    for (int i = 0; i < initializer_list.size(); i++) {
        array[i] = init<VALUE_TYPE, SUB_SIZE, SUB_SUB_SHAPE...>(std::data(initializer_list)[i]);
    }
    return array;
}

template<class VALUE_TYPE, uint32_t SIZE, uint32_t ... SUB_SHAPE>
struct ArrayND
{
    using InitType = std::initializer_list<typename ArrayND<VALUE_TYPE, SUB_SHAPE...>::InitType>;
    using DataType = std::array<typename ArrayND<VALUE_TYPE, SUB_SHAPE...>::DataType, SIZE>;

    constexpr explicit ArrayND(const InitType& initializer_list)
            : data(init<VALUE_TYPE, SIZE, SUB_SHAPE...>(initializer_list))
    {}

    DataType data;
};

template<class VALUE_TYPE, uint32_t SIZE>
struct ArrayND<VALUE_TYPE, SIZE>
{
    using InitType = std::initializer_list<VALUE_TYPE>;
    using DataType = std::array<VALUE_TYPE, SIZE>;

    constexpr explicit ArrayND(const InitType& initializer_list)
            : data(init<VALUE_TYPE, SIZE>(initializer_list))
    {}

    DataType data;
};

template<class VALUE_TYPE>
struct ArrayND<VALUE_TYPE, 1>
{
    using InitType = std::initializer_list<VALUE_TYPE>;
    using DataType = std::array<VALUE_TYPE, 1>;

    constexpr explicit ArrayND(const InitType& initializer_list)
            : data(init<VALUE_TYPE, 1>(initializer_list))
    {}

    DataType data;
};

下面是实际的用法:

constinit ArrayND<float, 1> my_scaler({11.11f});

constinit ArrayND<float, 3> my_2_array(
        { 0.1f, 0.2f, 0.3f }
);

constinit ArrayND<float, 2, 2> my_2x2_array(
        {{ 0.1f, 0.2f }, { 0.3f, 0.4f }}
);

constinit ArrayND<float, 3, 2, 1, 2> my_3x2x5x7_array(
    {
        {{ {0.1f, 0.2f} }, { {0.3f, 0.4f} }},
        {{ {0.1f, 0.2f} }, { {0.3f, 0.4f} }},
        {{ {0.1f, 0.2f} }, { {0.3f, 0.4f} }}
    }
);

感谢所有帮助的家伙!我仍然觉得可以使用递归可变参数模板参数和构造函数参数来做到这一点......但这肯定会让我回到正轨并解决我试图解决的问题

【讨论】:

    猜你喜欢
    • 2022-01-11
    • 2011-10-26
    • 2021-08-20
    • 1970-01-01
    • 1970-01-01
    • 2016-07-14
    • 2016-02-19
    • 2021-09-21
    • 2017-05-15
    相关资源
    最近更新 更多