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_assert
s 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 constexpr
s within the function definition, they would also be constexpr
s 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()
asconstexpr
? 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
Cygwin Make Bash Command Not Found
"Apientry _Twinmain" and "Winapi Winmain" Difference
C++ Stl: Array VS Vector: Raw Element Accessing Performance
Macro/Keyword Which Can Be Used to Print Out Method Name
Is There Any Advantage to Using Pow(X,2) Instead of X*X, with X Double
Can Nullptr Be Emulated in Gcc
When and Why Would You Use Static with Constexpr
Deterministic Builds Under Windows
What C++ Library Should I Use to Implement a Http Client
Thread Safety of Mpi Send Using Threads Created with Std::Async
Why Is Std::Is_Pod Deprecated in C++20
Check If One String Is a Prefix of Another
"Right" Way to Deallocate an Std::Vector Object
Compile Lua Code, Store Bytecode Then Load and Execute It
How to Receive a Lambda as Parameter by Reference