static_assert on initializer_list::size()
Asked Answered
M

5

41

Why is std::initializer_list<_E>::size not allowable in a static_assert, even though it's declared as a constexpr in my libstdc++ (v. 4.6)?

For example, the following code:

template<class T, int Length>
class Point
{
  public:
    Point(std::initializer_list<T> init)
    {
      static_assert(init.size() == Length, "Wrong number of dimensions");
    }
};

int main()
{
  Point<int, 3> q({1,2,3});

  return 0;
}

gives the following error:

test.C: In constructor ‘Point<T, Length>::Point(std::initializer_list<_Tp>) [with T = int, int Length = 3]’:
test.C:60:26:   instantiated from here
test.C:54:7: error: non-constant condition for static assertion
test.C:54:73:   in constexpr expansion of ‘init.std::initializer_list<_E>::size [with _E = int, std::initializer_list<_E>::size_type = long unsigned int]()’
test.C:54:7: error: ‘init’ is not a constant expression

Note that this works just fine for a trivial example:

class A
{
  public:
    constexpr int size() { return 5; }
};

int main()
{
  A a;
  static_assert(a.size() == 4, "oh no!");

  return 0;
}
Merchantman answered 25/3, 2011 at 22:12 Comment(5)
It does look like it should work the way that you want.Kassie
Yeah, I'm wondering if this is a compiler bug? I don't want to bother the gcc folks if I'm making a mistake here, but looking at the initializer_list header file leads me to believe that there is something wrong here.Merchantman
I understand that size() is declared as constexpr in libstdc++, but it should be noted that the Standard does not require this. So even if you got this to work (e.g. perhaps using Evgeny Panasyuk's approach below), you couldn't rely on this to work with other implementations of the Standard Library.Abba
Then again, it seems this is changing in C++14, see 18.9/1. Constructor, size(), begin() and end() are all declared as constexpr in the C++14 proposal.Abba
This still doesn't seem to work with Clang 3.5 and C++14. That's confusing.Swearingen
F
12

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?
Fairfield answered 25/3, 2011 at 22:31 Comment(7)
Yup, but that's the point - I would like to throw a static_assertion whenever someone calls the constructor with an ill-sized initializer list. Because the initializer_list is constructed by the compiler (and has no public constructor), and because the size() method is a constexpr, my static_assert should be totally possible.Merchantman
But there is only one constructor, so it cannot assert in some places and not in others. The static_assert, being compile time, cannot work differently for different calls to the constructor.Fairfield
Why not? Imagine that I had a templatted constructor that accepted an argument of type T2. It would be perfectly acceptable to put a static_assert to ensure that (for example) std::is_integral<T2>::value resolved to 'true'. This works because std::is_integral<T2>::value is a compile time constant, as is anything labeled constexpr. The reason that std::initializer_list::size() can be labeled constexpr is because it has a special private constructor that only the compiler can access.Merchantman
Yes, but it doesn't help that 'size()` is constexpr, when init isn't. The compiler complains about init.Fairfield
Yes, but then why does my simple example work? The object "a" is not constant, yet has a constexpr size() method which works just fine in the static_assert.Merchantman
Yes, but there is only one "a". There will be a new "init" for each Point object constructed.Fairfield
@Boatzart: The problem is, that the compiler generates distinct constructors for each T2, at compile time all different possibilities are already there. But you don't know if you pass an initializer_list of size 5 or one of size 3 at compile time. And the static assert is at compile time, so the compiler needs to know all different possibilities.Kinnikinnick
B
40

"Initializer lists" are just horrible kludges.

Don't:

#include <initializer_list>

template<typename T>
void Dont(std::initializer_list<T> list) { // Bad!
    static_assert(list.size() == 3, "Exactly three elements are required.");
}

void Test() { Dont({1,2,3}); }

Do:

template<typename T, std::size_t N>
void Do(const T(&list)[N]) { // Good!
    static_assert(N == 3, "Exactly three elements are required.");
}

void Test() { Do({1,2,3}); }
Bikini answered 31/7, 2016 at 1:12 Comment(15)
This seems to be working on gcc but not clang 3.6 or 3.7.Loci
@Quant: These are Clang bugs. Works in 3.8 onwards.Bikini
Thanks. Do you have a reference showing that it is a bug?Loci
@Quant: Besides the fact that it is valid C++ (and accepted by major compilers), I don't have any bugtrack link to quote. I do remember having seen similar bugs in Clang that might or might not relate to this, however.Bikini
const T(&)[N] What is this syntax? What do I need to search for to read more about it?Goggles
It should be const T(&list)[N]Grecoroman
@Ela782: In this context, const T(&)[N] is a type (lvalue reference to array of N elements of type const T). It is being used as an unnamed function parameter. If you want to assign the parameter a name, use const T (&name)[N].Bikini
@alexcov Hmm, why not const &T[N] then? Is that illegal syntax, but conceptually this is exactly what const T(&)[N] is?Goggles
@Goggles const &T[N] declares an array of references (which are illegal semantically), the parentheses are necessary to declare a reference to the array itself.Insuppressible
Wouldn't it make more sense here to use const T(&list)[3]? With the size hardcoded we wouldn't need the static_assert at all.Guthrey
@Rickard: Yes, that also works. A static_assert is still useful if you want a custom message in your error log.Bikini
But the question wasn't whether an initializer_list is good or bad (IMO is not that bad), but why doesn't it work in that context with a constexprDiviner
Which makes one wonder, what are std::initializer_list for after all? I don't even think one can have a dynamic initializer_list whose size couldn't be known at compile time. Seems that the only reason is to avoid code bloat (no template N). As a matter of fact it looks like a half-backed type erased (size-erased) of the general type T&[N]. Perhaps there some extra conversion rule on individual elements (but that can be done internally in the function).Prehistoric
@Rickard, the problem with const T(&list)[3] is that (most compilers?) will accept an argument with more than 3 elements. And might warn only if you are lucky with the warning options. This could be what you want, but with const T(&list)[N] you have full control on what to do, fail, ignore or even warn with a pragma.Prehistoric
Note that it is also passing on void Test() { Do({1,2}); }Ineffectual
F
12

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?
Fairfield answered 25/3, 2011 at 22:31 Comment(7)
Yup, but that's the point - I would like to throw a static_assertion whenever someone calls the constructor with an ill-sized initializer list. Because the initializer_list is constructed by the compiler (and has no public constructor), and because the size() method is a constexpr, my static_assert should be totally possible.Merchantman
But there is only one constructor, so it cannot assert in some places and not in others. The static_assert, being compile time, cannot work differently for different calls to the constructor.Fairfield
Why not? Imagine that I had a templatted constructor that accepted an argument of type T2. It would be perfectly acceptable to put a static_assert to ensure that (for example) std::is_integral<T2>::value resolved to 'true'. This works because std::is_integral<T2>::value is a compile time constant, as is anything labeled constexpr. The reason that std::initializer_list::size() can be labeled constexpr is because it has a special private constructor that only the compiler can access.Merchantman
Yes, but it doesn't help that 'size()` is constexpr, when init isn't. The compiler complains about init.Fairfield
Yes, but then why does my simple example work? The object "a" is not constant, yet has a constexpr size() method which works just fine in the static_assert.Merchantman
Yes, but there is only one "a". There will be a new "init" for each Point object constructed.Fairfield
@Boatzart: The problem is, that the compiler generates distinct constructors for each T2, at compile time all different possibilities are already there. But you don't know if you pass an initializer_list of size 5 or one of size 3 at compile time. And the static assert is at compile time, so the compiler needs to know all different possibilities.Kinnikinnick
P
6

From my discussion with @Evgeny, I realized that this just works (with gcc 4.8 c++11) and may as well do the size check by only accepting a compatible size in the initializer list (in main).

(code link: http://coliru.stacked-crooked.com/a/746e0ae99c518cd6)

#include<array>
template<class T, int Length>
class Point
{
  public:
    Point(std::array<T, Length> init)
    {
//not needed//      static_assert(init.size() == Length, "Wrong number of dimensions");
    }
};

int main()
{
  Point<int, 3> q({1,2,3}); //ok
//  Point<int, 3> q2({1,2,3,4}); //compile error (good!)
  Point<int, 3> q2({1,2}); // ok, compiles, same as {1,2,0}, feature or bug?
  return 0;
}
Prehistoric answered 12/11, 2013 at 11:12 Comment(1)
related: #33000322Prehistoric
C
2

Use following syntax:

LIVE DEMO

#include <initializer_list>

template<class T, int Length>
class Point
{
    std::initializer_list<T> data;
public:
    constexpr Point(std::initializer_list<T> init)
        : data
        (
            init.size() == Length ?
            init : throw 0
        )
    {}
};

int main()
{
    constexpr Point<int, 3> a{{1,2,3}};
    constexpr Point<int, 2> b{{1,2,3}}; // compile time error
}

Refer following SO.


EDIT: Interesting that works on GCC 4.8.1, but does not work on Clang 3.4. Maybe this is related to constexpr of .size() (afaik, in C++14 it is constexpr).

Cletacleti answered 8/4, 2013 at 21:29 Comment(8)
Interesting, can this condition be applied to a non constexpr function but to a constexpr argument (if that thing existed at all). For example void fun(double param, [constexpr] std::initializer_list<double> init){init.size()!=2?throw 0:other...code;}.Prehistoric
@Prehistoric Looks like yes - but parameter should be taken by value, const& does not work. (you don't need throw in such case, static_assert is enough. throw was used to overcome constexpr limitations). But looks like this works only for constexpr methods which do not depend on membersCletacleti
Well, I think I am trying to do something different, which it looks possible in principle, but I cannot do it. See the example: coliru.stacked-crooked.com/a/9f4a561a5fd9178a I want to check at compile time if a constexpr initializer_list has the right size.Prehistoric
I think this is a more complete example of what I want, coliru.stacked-crooked.com/a/e3fee72a1b5d6181, but seems to be impossible because referencing to initializer_list elements is not a constexpr anyhow. In summary I want to be able to call functions like this fun(runtime_var1, {1.,2.}) but if the second argument is a constexpr initializer_list then check at compile time tha the number of elements is two. I think this cannot be done unless constexpr can be specified for the each argument separately.Prehistoric
@Prehistoric I see what you mean, here is another example. Looks like in C++14, begin and end of initializer_list are constexpr. Per-argument constexpr may solve that problem too.Cletacleti
yes, I don't know, I cannot make it do anything useful, even with C++14 I am not very optimistic, since it seems that a function call won't be able to mix constexpr and non-constexpr arguments, and that is what I want. The ultimate reason is that I would like to C++ to accept hierarchical argument passing (by nested bracket, e.g. sum(&function, {0, 10}), even what I would call initializer_tuple (see here https://mcmap.net/q/393055/-is-it-possible-to-infer-template-parameters-of-tuple-from-brace-type-initialization/225186). I don't see intrinsic limitations in the language for these extended syntaxes.Prehistoric
@Prehistoric I am not sure, perhaps initializer list really should return something like std::array<T, N> instead of std::initializer_list<T>.Cletacleti
"initializer_list<T, N>" was discussed here #7108925 . And yes, you might be correct that an array<T, 2> could be the whole answer I was looking for because this works: coliru.stacked-crooked.com/a/8fe9af280f3baffc. I think I gave up with this in C++98 (and boost::array) because it didn't work in the pass, but now it works!!. (Now the remaining thing is to wait for initializer_tuple in C++2x.)Prehistoric
L
0

I haven't really figured out what's going on here.

If I say

const std::initializer_list<double> li = { 1, 2.5, 3.7, 4.3 };
static_assert(li.size() == 4, "fail");

I get a complain that 'li' was not declared 'constexper'.

But if I say

constexpr std::size_t dSize(std::initializer_list<double> di)
{
    return di.size();
}

then

static_assert(dSize({1, 2.5, 3.7, 4.3}) == 4, "failed");   // works

but

static_assert(dSize(li) == 4, "failed");

fails with "error: the value of 'li' is not usable in a constant expression"

This is all with -std=c++11

So, somehow, an initializer list passed into an arg list can be part of a constant expression, but an initializer list just declared as a const variable can't.

Lazes answered 15/10, 2020 at 21:7 Comment(3)
constexpr (function dSize) doesn't mean it will return a constexpr. It will "try" to do so if certain conditions are met (probably never with std::initializer_list it seems) that is why the last static_assert doesn't work. constexpr means different things when applied to variables and when applied to functions.Prehistoric
I figured out what I was doing wrong. When I declared the 'constexpr' init list, I was doing it within 'main' so it was an automatic variable. When I moved the declaration outside of any function, so that it was a global or static variable, then the 'constexpr' worked correctly and the static asserts worked.Lazes
that is interesting actually. Although a global initializer_list wouldn't be very useful.Prehistoric

© 2022 - 2024 — McMap. All rights reserved.