frozenca
frozenca

Reputation: 877

Is there way to construct from initializer_list in compile time?

I'm writing C++ ndarray class. I need both dynamic-sized and compile-time-size-known arrays (free store allocated and stack allocated, respectively). I want to support initializing from nested std::initializer_list.

Dynamic-sized one is OK: This works perfectly.

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

Static-sized one is also OK: This works fine.

    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

But I hate that the initialization is done in runtime actually. I want to make it as compile-time operation. My implementation detail is as follows:

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

addList and checkNonJagged doesn't work as compile-time, because the compiler thinks that init is not known as compile time even within the example case.

How can I make this as compile-time operation?

Upvotes: 4

Views: 1646

Answers (1)

qqNade
qqNade

Reputation: 1982

Yes, there's no reason why constexpr std::initializer_list would be unusable in compile-time initialization.

From your code snippet, it's unclear whether you've used an in-class initialization for StaticArray members, so one of the issues you could've run into is that a constexpr constructor can't use a trivial constructor for members which would initialize them to unspecified run-time values.

So the fix for your example is to default-initialize StaticArray members and specify constexpr for the constructor, checkDims, addList and data. To initialize a runtime StaticArray with constexpr std::initializer_list validated at compile-time, you can make the initializer expression manifestly constant-evaluated using an immediate function.

As you probably realize, it is impossible to initialize a run-time variable at compile-time so that's the best one can do.

If what you wanted is to validate at compile-time the dimensions of an std::initializer_list that depends on runtime variables, it can't be done -- the std::initializer_list is not constexpr, so its size isn't either. Instead, you can define a wrapper type around Scalar, mark its default constructor as deleted, and accept an aggregate type of these wrappers in the StaticArray constructor, for example a nested std::array of the desired dimensions, or, to avoid double braces, a C-style multidimensional array. Then, if the dimensions don't match, compilation will fail for either of the two reasons: too many initializers or the use of the deleted default constructor.

The code below compiles on godbolt with every GCC, Clang, MSVC version that supports C++20.

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

Upvotes: 2

Related Questions