Static_Assert on Initializer_List::Size()

static_assert on initializer_list::size()

The compiler says that init is the problem, not init.size().

I guess that the constructor could be called from different places with different length initializers.

(To elaborate: You're trying to write a static_assert that depends on the run-time value of the variable init, namely how many elements it has. static_asserts have to be evaluable at the time the function is compiled. Your code is analogous to this trivially invalid example:)

void foo(int i) { static_assert(i == 42, ""); }
int main() { foo(42); } // but what if there's a caller in another translation unit?

`static_assert` on `std::initializer_listT::size` in a function

The reason this fails even though size is constexpr is because list is not a constexpr variable so any member function calls on it will also not be constexpr.

All is not lost though. What you can do is use a std::array instead of a std::initializer_list which lets you even get rid of the static_assert like:

template <typename T, std::size_t N>
constexpr void set_array(T (&x)[N], std::array<T, N>&& list) {
for (std::size_t i = 0; i < N; ++i)
x[i] = list[i];
}

int main()
{
int arr[4];
set_array(arr, {1,2,3,4});
std::cout << arr[3];
}

If you tried using

set_array(arr, {1,2,3,4,5});

then you would get a compiler error like

main.cpp:12:16: note: candidate function [with T = int, N = 4] not viable: cannot convert initializer list argument to 'std::array<int, 4UL>'
constexpr void set_array(T (&x)[N], std::array<T, N>&& list) {

How to static_assert that an initializer list is a certain size

Although the size() of an std::initializer_list<T> can evaluate to a constexpr the size() member won't behave like a constexpr within a constexpr function: it is intentional that objects only behave like constexpr within the constexpr expression where they were introduce but not elsewhere.

For example:

constexpr get_size(std::initializer_list<int> list) {
constexpr std::size_t csize = list.size(); // ERROR (1)
std::size_t size = list.size(); // OK
return size;
}

int main() {
constexpr std::size_t csize = get_size(std::initializer_list<int>{ 1, 2, 3 }); // OK (2)
// ...
}

In the first case (1) the value which is assumed to yield a constexpr depends on data created before the constexpr starts. As a result, it doesn't evaluate to a constexpr. In the second case (2) the data is defined within the constexpr and, thus, the result can become a constexpr.

I haven't been part of the discussions leading to this design but it seems it is motivated by the desire to prevent constexpr arguments changing the result type of a constexpr function: if the values were constexprs within the function definition, they would also be constexprs in the return value and could, thus, be used as template arguments in the return type. That would lead to different values to constexpr function yielding different types. So far, you can get a different return type only by varying the argument types of a function and/or by changing the number of arguments of a function.

constexpr-ness of std::initializer_list::size() vs std::array::size()

Why isn't std::initializer_list implemented in a way such that (1) compiles? Is there something in the standard that prevents such an implementation?

Yes, it's impossible. An initializer_list can have any size, you cannot during constant evaluation time get the size of an arbitrary runtime initializer_list. This is quite different from std::array, where a given std::array<T, N> has size N. One's size is variable, the other's is fixed.

This isn't really different from any other variable:

struct X { int i; };

X x{42};
constexpr X cx{17};

constexpr int i = x.i; // error
constexpr int ci = cx.i; // ok

Given that (1) fails, what is the purpose of marking std::initializer_list::size() as constexpr? The only use case seems to be this one

This is not the only use case, far from. constexpr member functions do not just permit you to invoke them on constexpr objects. They more generally permit you to invoke them anywhere during constant evaluation time.

That is, during any kind of constant evaluation, if you create an initializer_list, you can then use its size. A silly minimal example might be:

constexpr size_t four() {
std::initializer_list<int> lst = {1, 2, 3, 4};
return lst.size();
}

static_assert(four() == 4);

Note that lst itself is not a constexpr object, it's just some ephemeral thing that got created during the course of evaluating the call to four() during constant evaluation. But we still need that size() to be constexpr - invoking any non-constexpr function is a no-no.

From here you can extend it outwards to any arbitrary code you might want to run that at some point, during constant evaluation, wants to determine the size of an std::initializer_list.

get the size of `std::initializer_list` at compile time

Maybe its a bad idea to use std::initializer_list?

I think so. Consider

std::initializer_list<int> t;
std::initializer_list<int> a = {1,2,3};
std::initializer_list<int> b = {2,3};
if (rand() > 42)
t = a;
else
t = b;
auto reshaped_array = forreshape({1,2,3,4,5,6}, t);

In the above example, it's just impossible to know t.size() at compile time.

But you can ask the compiler to deduce the length of an initializer list by using a reference to C-style array, thanks to CWG 1591.

The definition of forreshape would look like:

template<int D, typename T>
auto forreshape(const std::initializer_list<T> & values, const int (&shape)[D])
{
// use D...
}

Initializer List construction and static assert

The problem of solution you posted is that in the list initialization, i.e. Queue<int,10> r = {66,55,44};, the constructors of Queue are considered in overload resolution to match the three arguments 66, 55, 44, then the match fails.

You can add two more braces as the workaround, then the argument {66,55,44} will be used as one argument to match the constructors' parameter; then it matches the one takes an array and works as expected.

Queue<int,10> r = {{66,55,44}};

Is there way to construct from initializer_list in compile time?

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

Validate contents of std::initializer_list at compile time

After some digging it looks like using std::initializer_list is not possible in GCC 4.7 due to the lack of constexpr in it's declaration. It should work with GCC 4.8 as <initializer_list> has been updated to include constexpr. Unfortunately using GCC 4.8 is not an option at the moment.

It is possible to access elements of an array if the decayed pointer is passed by reference though. This allows the validation to occur as desired but still isn't quite the solution I am hoping for. The following code is a workable solution for arrays. It still requires that the size of the array be supplied to the validation function but that it easy enough to correct.

#include <initializer_list>

template<typename T>
constexpr bool Compare(T& data, int size, int needleIndex, int haystackIndex)
{
return
needleIndex == haystackIndex ?
Compare(data, size, needleIndex + 1, haystackIndex)
: needleIndex == size ?
false
: data[needleIndex] == data[haystackIndex] ?
true
: Compare(data, size, needleIndex + 1, haystackIndex);
}

template<typename T>
constexpr bool Compare(T& data, int size, int index)
{
return
index == size ?
false
: Compare(data, size, index + 1) ?
true
: Compare(data, size, 0, index);
}

template<typename T, int ArraySize>
constexpr bool Validate(T(&input)[ArraySize], int size)
{
return !Compare(input, size, 0);
}

int main()
{
constexpr int initData0[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
constexpr int initData1[] = {1, 1, 2, 3, 4, 5, 6, 7, 8, 9};
constexpr int initData2[] = {2, 1, 2, 3, 4, 5, 6, 7, 8, 9};
constexpr int initData3[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 8};
constexpr int initData4[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 7};
constexpr int initData5[] = {0, 1, 0, 3, 4, 5, 6, 7, 8, 9};
constexpr int initData6[] = {0, 1, 2, 3, 4, 5, 6, 9, 8, 9};

static_assert(Validate(initData0, 10), "set 0 failed"); // <-- PASS
static_assert(Validate(initData1, 10), "set 1 failed"); // <-- (and below) FAIL
static_assert(Validate(initData2, 10), "set 2 failed");
static_assert(Validate(initData3, 10), "set 3 failed");
static_assert(Validate(initData4, 10), "set 4 failed");
static_assert(Validate(initData5, 10), "set 5 failed");
static_assert(Validate(initData6, 10), "set 6 failed");
}

.

Build log:

C:\Source\SwitchCaseString\main.cpp: In function 'int main()':

C:\Source\SwitchCaseString\main.cpp:198:2: error: static assertion failed: set 1 failed

C:\Source\SwitchCaseString\main.cpp:199:2: error: static assertion failed: set 2 failed

C:\Source\SwitchCaseString\main.cpp:200:2: error: static assertion failed: set 3 failed

C:\Source\SwitchCaseString\main.cpp:201:2: error: static assertion failed: set 4 failed

C:\Source\SwitchCaseString\main.cpp:202:2: error: static assertion failed: set 5 failed

C:\Source\SwitchCaseString\main.cpp:203:2: error: static assertion failed: set 6 failed



Related Topics



Leave a reply



Submit