有没有办法在编译时从 initializer_list 构造?

Is there way to construct from initializer_list in compile time?

我正在编写 C++ ndarray class。我需要动态大小和编译时大小已知的数组(分别分配自由存储和分配堆栈)。 我想支持从嵌套 std::initializer_list.

初始化

动态大小的可以:这很完美。

    frozenca::Array<float, 2> arr {{1, 2, 3}, {4, 5, 6}};
    std::cout << arr.size() << '\n'; // 6
    std::cout << arr[{1, 1}] << '\n'; // 5

静态大小的也可以:这很好用。

    frozenca::StaticArray<float, 2, 3> arr2 {{1, 2, 3}, {4, 5, 6}};
    std::cout << arr2.size() << '\n'; // 6
    std::cout << arr2[{1, 1}] << '\n'; // 5
    // static_assert(arr2[{1, 1}] == 5); // THIS DOES NOT WORK

但我讨厌初始化实际上是在运行时完成的。我想把它作为编译时操作。 我的实现细节如下:

template <std::semiregular T, std::size_t N>
using DenseInitializer_t = typename DenseInitializer<T, N>::type;

template <std::semiregular T, std::size_t N>
struct DenseInitializer {
    using type = std::initializer_list<DenseInitializer_t<T, N - 1>>;
};

template <std::semiregular T>
struct DenseInitializer<T, 1> {
    using type = std::initializer_list<T>;
};

template <std::semiregular T>
struct DenseInitializer<T, 0>;

template <std::semiregular Scalar, int... Sizes>
template <typename Initializer>
constexpr void StaticArray<Scalar, Sizes...>::verifyDims(const Initializer& init) const {
    checkDims<0, Initializer, Sizes...>(init);
}

template <std::semiregular Scalar, int... Sizes>
StaticArray<Scalar, Sizes...>::StaticArray(DenseInitializer_t<value_type, N> init) {
    verifyDims(init);
    insertFlat(data(), init);
}

template <typename Initializer>
constexpr bool checkNonJagged(const Initializer& init) {
    auto i = std::cbegin(init);
    return std::all_of(init.begin(), init.end(), [&i](const auto& it) {
        return it.size() == i->size();
    });
}

template <int k, typename Initializer, int... Sizes>
void checkDims(const Initializer& init) {
    if constexpr (k < sizeof...(Sizes) - 1) {
        if (!checkNonJagged(init)) {
            throw std::invalid_argument("Jagged matrix initializer");
        }
    }
    if (std::get<k>(std::forward_as_tuple(Sizes...)) != std::ssize(init)) {
        throw std::invalid_argument("Matrix initializer does not match with static matrix");
    }
    if constexpr (k < sizeof...(Sizes) - 1) {
        checkDims<k + 1, decltype(*std::begin(init)), Sizes...>(*std::begin(init));
    }
}

template <std::semiregular T>
void addList(T* data,
             const T* first, const T* last,
             int& index) {
    for (; first != last; ++first) {
        data[index] = *first;
        ++index;
    }
}

template <std::semiregular T, typename I>
void addList(T* data,
             const std::initializer_list<I>* first, const std::initializer_list<I>* last,
             int& index) {
    for (; first != last; ++first) {
        addList(data, first->begin(), first->end(), index);
    }
}

template <std::semiregular T, typename I>
void insertFlat(T* data, std::initializer_list<I> list) {
    int index = 0;
    addList(data, std::begin(list), std::end(list), index);
}

addListcheckNonJagged 不能用作编译时,因为编译器认为 init 即使在示例案例中也不被称为编译时。

如何将其作为编译时操作?

是的,constexpr std::initializer_list 没有理由在 compile-time 初始化中不可用。

从您的代码片段中,不清楚您是否对 StaticArray 成员使用了 in-class 初始化,因此您可能 运行 遇到的问题之一是 constexpr 构造函数不能' t 为成员使用普通构造函数,将它们初始化为未指定的 run-time 值。

因此,您的示例的解决方法是 default-initialize StaticArray 成员并为构造函数、checkDims、addList 和数据指定 constexpr。要使用在 compile-time 处验证的 constexpr std::initializer_list 初始化 运行time StaticArray,您可以使用立即函数 constant-evaluated 使初始化表达式显式 constant-evaluated。

正如您可能意识到的那样,不可能在 compile-time 处初始化一个 run-time 变量,所以这是最好的办法。

如果您想要在 compile-time 处验证依赖于 运行 时间变量的 std::initializer_list 的维度,则无法完成 -- std::initializer_list 不是 constexpr,所以它的大小也不是。相反,您可以围绕 Scalar 定义一个包装器类型,将其默认构造函数标记为已删除,并在 StaticArray 构造函数中接受这些包装器的聚合类型,例如所需维度的嵌套 std::array,或者,为了避免双重大括号,一个 C-style 多维数组。然后,如果尺寸不匹配,编译将因以下两个原因之一而失败:太多的初始化程序或使用已删除的默认构造函数。

下面的代码在 godbolt 上使用支持 C++20 的每个 GCC、Clang、MSVC 版本编译。

#include <algorithm>
#include <array>
#include <concepts>
#include <iostream>
#include <numeric>
#include <tuple>
#include <utility>

namespace frozenca {

template <std::size_t sz0, std::size_t... sz>
constexpr std::size_t prod() {
    if constexpr (sizeof...(sz) == 0) {
        return sz0;
    } else {
        return sz0 * prod<sz...>();
    }
}

template <std::semiregular T, std::size_t N>
struct DenseInitializer;

template <std::semiregular T, std::size_t N>
using DenseInitializer_t = typename DenseInitializer<T, N>::type;

template <std::semiregular T, typename I>
constexpr void insertFlat(T* data, std::initializer_list<I> list);

template <int k, typename Initializer, int... Sizes>
constexpr void checkDims(const Initializer& init);

template <std::semiregular Scalar, int... Sizes>
struct StaticArray {
    using value_type = Scalar;
    static constexpr std::size_t N = sizeof...(Sizes);

    Scalar body[prod<Sizes...>()];

    constexpr Scalar* data() {
        return body;
    }

    constexpr std::size_t size() const {
        return std::size(body);
    }

    // no bound checks performed
    constexpr Scalar operator[](const std::array<std::size_t, N>& index) const {
        std::size_t dim = 0, idx = 0;
        ((idx = idx * Sizes + index[dim++]), ...);
        return body[idx];
    }

    void print() const {
        for (const auto& i: body) {
            std::cout << i << ' ';
        }
        std::cout << std::endl;
    }

    template <typename Initializer>
    constexpr void verifyDims(const Initializer& init) const;

    constexpr StaticArray(DenseInitializer_t<value_type, N> init);
};


template <std::semiregular T, std::size_t N>
struct DenseInitializer {
    using type = std::initializer_list<DenseInitializer_t<T, N - 1>>;
};

template <std::semiregular T>
struct DenseInitializer<T, 1> {
    using type = std::initializer_list<T>;
};

template <std::semiregular T>
struct DenseInitializer<T, 0>;

template <std::semiregular Scalar, int... Sizes>
template <typename Initializer>
constexpr void StaticArray<Scalar, Sizes...>::verifyDims(const Initializer& init) const {
    checkDims<0, Initializer, Sizes...>(init);
}

template <std::semiregular Scalar, int... Sizes>
constexpr StaticArray<Scalar, Sizes...>::StaticArray(DenseInitializer_t<value_type, N> init) : body{} {
    verifyDims(init);
    insertFlat(data(), init);
}

template <typename Initializer>
constexpr bool checkNonJagged(const Initializer& init) {
    auto i = std::cbegin(init);
    return std::all_of(init.begin(), init.end(), [&i](const auto& it) {
        return it.size() == i->size();
    });
}

template <int k, typename Initializer, int... Sizes>
constexpr void checkDims(const Initializer& init) {
    if constexpr (k < sizeof...(Sizes) - 1) {
        if (!checkNonJagged(init)) {
            throw std::invalid_argument("Jagged matrix initializer");
        }
    }
    if (std::get<k>(std::forward_as_tuple(Sizes...)) != std::ssize(init)) {
        throw std::invalid_argument("Matrix initializer does not match with static matrix");
    }
    if constexpr (k < sizeof...(Sizes) - 1) {
        checkDims<k + 1, decltype(*std::begin(init)), Sizes...>(*std::begin(init));
    }
}

template <std::semiregular T>
constexpr void addList(T* data,
                       const T* first, const T* last,
                       int& index) {
    for (; first != last; ++first) {
        data[index] = *first;
        ++index;
    }
}

template <std::semiregular T, typename I>
constexpr void addList(T* data,
                       const std::initializer_list<I>* first, const std::initializer_list<I>* last,
                       int& index) {
    for (; first != last; ++first) {
        addList(data, first->begin(), first->end(), index);
    }
}

template <std::semiregular T, typename I>
constexpr void insertFlat(T* data, std::initializer_list<I> list) {
    int index = 0;
    addList(data, std::begin(list), std::end(list), index);
}

}

consteval auto echo(std::copy_constructible auto val) {
    return val;
}

void check0() {
    // frozenca::StaticArray<float, 2, 3> arr_jagged {{1, 2, 3}, {4, 5}}; // throws an exception
    frozenca::StaticArray<float, 2, 3> arr2 {{1, 2, 3}, {4, 5, 6}};
    std::cout << arr2.size() << '\n'; // 6
    std::cout << arr2[{1, 1}] << '\n'; // 5
    // static_assert(arr2[{1, 1}] == 5); // THIS DOES NOT WORK
    arr2.print();
}

void check1() {
    // constexpr frozenca::StaticArray<float, 2, 3> arr_jagged {{1, 2, 3}, {4, 5}}; // compile-time error
    constexpr frozenca::StaticArray<float, 2, 3> arr2 {{1, 2, 3}, {4, 5, 6}};
    static_assert(arr2.size() == 6);
    static_assert(arr2[{1, 1}] == 5);
    arr2.print();
}

void check2() {
    // auto arr_jagged = echo(frozenca::StaticArray<float, 2, 3>{{1, 2, 3}, {4, 5}}); // compile-time error
    auto arr2 = echo(frozenca::StaticArray<float, 2, 3>{{3, 2, 1}, {6, 5, 4}});
    std::cout << arr2.size() << '\n'; // 6
    std::cout << arr2[{1, 1}] << '\n'; // 5
    // static_assert(arr2[{1, 1}] == 5); // THIS DOES NOT WORK
    arr2.print();
}

namespace aggregate {

struct NoDefault {
  int val;

  constexpr NoDefault() = delete;
  constexpr NoDefault(int val) : val{val} {};
  constexpr operator int() const { return val; }
};

template <std::size_t... sizes>
struct NDNested;

template <std::size_t... sizes>
using NDNested_t = typename NDNested<sizes...>::type;

template <>
struct NDNested<> {
    using type = NoDefault;
};

template <std::size_t size0, std::size_t... sizes>
struct NDNested<size0, sizes...> {
    using type = NDNested_t<sizes...>[size0];
};

template <std::size_t sz0, std::size_t... sizes>
constexpr int sum(const NDNested_t<sz0, sizes...>& t) {
    if constexpr (sizeof...(sizes) != 0) {
        constexpr auto op = [](int acc, const auto& arr) { return acc + sum<sizes...>(arr); };
        return std::accumulate(std::begin(t), std::end(t), 0, op);
    } else {
        return std::accumulate(std::begin(t), std::end(t), 0);
    }
}

}

void check_aggregate() {
    using aggregate::sum;
#ifndef _MSC_VER
    static_assert(sum<2, 2, 2>({{{1, 1}, {2, 4}}, {{1, 2}, {3, 4}}}) == 18);
#endif
    int x = 100;
    std::cout << sum<2, 2, 2>({{{1, 1}, {2, 4}}, {{1, 2}, {x, 4}}}) << '\n';
    // std::cout << sum<2, 2, 2>({{{1, 1}, {2, 4}}, {{1, 2}, {x}}}) << '\n'; // deleted constructor
    // std::cout << sum<2, 2, 2>({{{1, 1}, {2, 4}}, {{1, 2}, {x, 3, 4}}}) << '\n'; // excess elements
}

int main() {
    check0();
    check1();
    check2();
    check_aggregate();
}