39

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;
}
alfC
  • 14,261
  • 4
  • 67
  • 118
rcv
  • 6,078
  • 9
  • 43
  • 63
  • It does look like it should work the way that you want. – Zan Lynx Mar 26 '11 at 00:51
  • 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. – rcv Mar 26 '11 at 20:52
  • 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. – jogojapan May 25 '13 at 05:59
  • 2
    Then again, it seems this is changing in [C++14](http://isocpp.org/files/papers/N3690.pdf), see 18.9/1. Constructor, `size()`, `begin()` and `end()` are all declared as `constexpr` in the C++14 proposal. – jogojapan May 25 '13 at 06:40
  • 1
    This still doesn't seem to work with Clang 3.5 and C++14. That's confusing. – ChristopherC Dec 15 '14 at 04:04

5 Answers5

37

"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}); }
Danvil
  • 22,240
  • 19
  • 65
  • 88
alecov
  • 4,882
  • 2
  • 29
  • 55
  • This seems to be working on gcc but not clang 3.6 or 3.7. – Quant Dec 14 '16 at 22:07
  • 1
    @Quant: These are Clang bugs. Works in 3.8 onwards. – alecov Dec 15 '16 at 16:00
  • Thanks. Do you have a reference showing that it is a bug? – Quant Dec 15 '16 at 22:47
  • @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. – alecov Dec 29 '16 at 13:55
  • 9
    `const T(&)[N]` What is this syntax? What do I need to search for to read more about it? – Ela782 Jul 25 '18 at 20:28
  • 1
    It should be `const T(&list)[N]` – Fan Jul 25 '18 at 21:38
  • 5
    @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]`. – alecov Jul 26 '18 at 03:09
  • 1
    @alexcov Hmm, why not `const &T[N]` then? Is that illegal syntax, but conceptually this is exactly what `const T(&)[N]` is? – Ela782 Jul 26 '18 at 20:14
  • 3
    @Ela782 `const &T[N]` declares an array of references (which are illegal semantically), the parentheses are necessary to declare a reference to the array itself. – Jake Cobb Sep 08 '18 at 14:55
  • 2
    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. – Rickard Jul 31 '19 at 17:46
  • @Rickard: Yes, that also works. A `static_assert` is still useful if you want a custom message in your error log. – alecov Jul 31 '19 at 18:41
  • 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 constexpr – Triskeldeian Apr 05 '20 at 19:55
  • 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). – alfC Nov 06 '20 at 04:41
  • 1
    @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. – alfC Nov 06 '20 at 04:44
  • Note that it is also passing on `void Test() { Do({1,2}); }` – hojin Jul 23 '21 at 04:18
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?
Quuxplusone
  • 23,928
  • 8
  • 94
  • 159
Bo Persson
  • 90,663
  • 31
  • 146
  • 203
  • 3
    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. – rcv Mar 26 '11 at 00:13
  • 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. – Bo Persson Mar 26 '11 at 05:32
  • 3
    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::value resolved to 'true'. This works because std::is_integral::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. – rcv Mar 26 '11 at 16:51
  • 1
    Yes, but it doesn't help that 'size()` is constexpr, when `init` isn't. The compiler complains about `init`. – Bo Persson Mar 27 '11 at 12:47
  • 1
    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. – rcv Mar 28 '11 at 19:52
  • 1
    Yes, but there is only one "a". There will be a new "init" for each Point object constructed. – Bo Persson Mar 28 '11 at 19:59
  • 1
    @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. – Xeo Mar 28 '11 at 20:00
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;
}
alfC
  • 14,261
  • 4
  • 67
  • 118
  • related: http://stackoverflow.com/questions/32999822/is-there-a-way-to-enforce-full-initialization-of-stdarray – alfC Oct 12 '15 at 08:59
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).

Community
  • 1
  • 1
Evgeny Panasyuk
  • 9,076
  • 1
  • 33
  • 54
  • 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 init){init.size()!=2?throw 0:other...code;}`. – alfC Nov 11 '13 at 16:39
  • @alfC Looks like [yes](http://coliru.stacked-crooked.com/a/3e93b043398f5884) - but parameter should be taken by value, [`const&`](http://coliru.stacked-crooked.com/a/bf1bfb9d7f229aee) 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 members](http://coliru.stacked-crooked.com/a/a00a126ba1a216eb) – Evgeny Panasyuk Nov 11 '13 at 17:02
  • Well, I think I am trying to do something different, which it looks possible in principle, but I cannot do it. See the example: http://coliru.stacked-crooked.com/a/9f4a561a5fd9178a I want to check at compile time if a `constexpr initializer_list` has the right size. – alfC Nov 11 '13 at 20:31
  • I think this is a more complete example of what I want, http://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. – alfC Nov 11 '13 at 20:54
  • @alfC I see what you mean, here is [another example](http://coliru.stacked-crooked.com/a/63bf8303e5adddfa). Looks like in C++14, `begin` and `end` of `initializer_list` [are `constexpr`](http://en.cppreference.com/w/cpp/utility/initializer_list/begin). Per-argument `constexpr` may solve that problem too. – Evgeny Panasyuk Nov 12 '13 at 05:25
  • 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 http://stackoverflow.com/q/16703838/225186). I don't see intrinsic limitations in the language for these extended syntaxes. – alfC Nov 12 '13 at 09:43
  • 2
    @alfC I am not sure, perhaps initializer list really should return something like `std::array` instead of `std::initializer_list`. – Evgeny Panasyuk Nov 12 '13 at 10:23
  • 2
    "`initializer_list`" was discussed here http://stackoverflow.com/questions/7108425/why-is-the-size-not-a-template-argument-of-stdinitializer-list . And yes, you might be correct that an `array` could be the whole answer I was looking for because this works: http://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.) – alfC Nov 12 '13 at 10:58
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.

Bill Chapman
  • 71
  • 1
  • 3
  • 1
    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. – alfC Nov 06 '20 at 04:06
  • 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. – Bill Chapman Nov 08 '20 at 02:49
  • that is interesting actually. Although a global `initializer_list` wouldn't be very useful. – alfC Nov 08 '20 at 03:36