Reputation: 877
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
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