In the following code (tested locally and on Wandbox):
#include <iostream>
enum Types
{
A, B, C, D
};
void print(std::initializer_list<Types> types)
{
for (auto type : types)
{
std::cout << type << std::endl;
}
}
int main()
{
constexpr auto const group1 = { A, D };
print(group1);
return 0;
}
MSVC 15.8.5 fails to compile with:
error C2131: expression did not evaluate to a constant
note: failure was caused by a read of a variable outside its lifetime
note: see usage of '$S1'
(all referring to the line containing constexpr
)
Clang 8 (HEAD) reports:
error: constexpr variable 'group1' must be initialized by a constant expression
constexpr auto const group1 = { A, D };
^ ~~~~~~~~
note: pointer to subobject of temporary is not a constant expression
note: temporary created here
constexpr auto const group1 = { A, D };
^
gcc 9 (HEAD) reports:
In function 'int main()':
error: 'const std::initializer_list<const Types>{((const Types*)(&<anonymous>)), 2}' is not a constant expression
18 | constexpr auto const group1 = { A, D };
| ^
error: could not convert 'group1' from 'initializer_list<const Types>' to 'initializer_list<Types>'
19 | print(group1);
| ^~~~~~
| |
| initializer_list<const Types>
Why?
Firstly, they all apparently consider enum-ids to be non-constant, despite them obviously actually being well-known compile-time constant values.
Secondly, MSVC complains about read outside lifetime, but the lifetime of group1
and its values should extend throughout its usage in print
.
Thirdly, gcc has a weird const-vs-non-const complaint that I can't make any sense of, since initialiser lists are always const.
Finally, all but gcc will happily compile and run this code without any problems if constexpr
is removed. Granted it is not necessary in this case, but I can't see any good reason for it not to work.
Meanwhile gcc will only compile and run the code if the parameter type is changed to std::initializer_list<const Types>
-- and making this change causes it to fail to compile in both MSVC and clang.
(Interestingly: gcc 8, with the parameter type change, does successfully compile and run the code including constexpr
, where gcc 9 errors out.)
FWIW, changing the declaration to this:
constexpr auto const group1 = std::array<Types, 2>{ A, D };
Does compile and run on all three compilers. So it is probably the initializer_list
itself that is misbehaving rather than the enum values. But the syntax is more annoying. (It's slightly less annoying with a suitable make_array
implementation, but I still don't see why the original isn't valid.)
constexpr auto const group1 = std::array{ A, D };
Also works, thanks to C++17 template induction. Though now print
can't take an initializer_list
; it has to be templated on a generic container/iterator concept, which is inconvenient.
When you initialize a std::initializer_list
it happens like this:
[dcl.init.list] (emphasis mine)
5 An object of type std::initializer_list is constructed from an initializer list as if the implementation generated and materialized a prvalue of type “array of N const E”, where N is the number of elements in the initializer list. Each element of that array is copy-initialized with the corresponding element of the initializer list, and the std::initializer_list object is constructed to refer to that array. [ Note: A constructor or conversion function selected for the copy shall be accessible in the context of the initializer list. — end note ] If a narrowing conversion is required to initialize any of the elements, the program is ill-formed. [ Example:
struct X { X(std::initializer_list<double> v); }; X x{ 1,2,3 };
The initialization will be implemented in a way roughly equivalent to this:
const double __a[3] = {double{1}, double{2}, double{3}}; X x(std::initializer_list<double>(__a, __a+3));
assuming that the implementation can construct an initializer_list object with a pair of pointers. — end example ]
How that temporary array gets used to initialize the std::initializer_list
is what determines if the initializer_list
is initialized with a constant expression. Ultimately, according to example (despite being non-normative), that initialization is going to take the address of the array, or its first element, which will produce a value of a pointer type. And that is not a valid constant expression.
[expr.const] (emphasis mine)
5 A constant expression is either a glvalue core constant expression that refers to an entity that is a permitted result of a constant expression (as defined below), or a prvalue core constant expression whose value satisfies the following constraints:
- if the value is an object of class type, each non-static data member of reference type refers to an entity that is a permitted result of a constant expression,
- if the value is of pointer type, it contains the address of an object with static storage duration, the address past the end of such an object ([expr.add]), the address of a function, or a null pointer value, and
- if the value is an object of class or array type, each subobject satisfies these constraints for the value.
An entity is a permitted result of a constant expression if it is an object with static storage duration that is either not a temporary object or is a temporary object whose value satisfies the above constraints, or it is a function.
If the array was however a static object, then that initializer would constitute a valid constant expression that can be used to initialize a constexpr
object. Since std::initializer_list
has an effect of lifetime extension on that temporary by [dcl.init.list]/6, when you declare group1
as a static object, clang and gcc seem allocate the array as a static object too, which makes the initialization's well-formedness subject only to whether or not std::initializer_list
is a literal type and the constructor being used is constexpr
.
Ultimately, it's all a bit murky.
It appears std::initializer_list
does not yet (in C++17) fulfill the requirements of literal type (which is a requirement the type of a constexpr
variable has to satisfy).
A discussion on whether it does so in C++14 is found in this post: Why isn't std::initializer_list
defined as a literal type?
which itself was a followup to the post discussing Is it legal to declare a constexpr initializer_list
object?
I compared the citations provided in the C++14 related post (of the C++14 standard) to the final working draft (of the C++17 standard) and they are the same.
So there is no explicit requirement that std::initializer_list
should be a literal type.
Citations from the final working draft of C++17 (n4659):
[basic.types]/10.5
(10.5) a possibly cv-qualified class type (Clause 12) that has all of the following properties:
(10.5.1) — it has a trivial destructor,
(10.5.2) — it is either a closure type (8.1.5.1), an aggregate type (11.6.1), or has at least one constexpr constructor or constructor template (possibly inherited (10.3.3) from a base class) that is not a copy or move constructor,
(10.5.3) — if it is a union, at least one of its non-static data members is of non-volatile literal type, and
(10.5.4) — if it is not a union, all of its non-static data members and base classes are of non-volatile literal types.
[initializer_list.syn]/1
- An object of type initializer_list provides access to an array of objects of type const E. [ Note:A pair of pointers or a pointer plus a length would be obvious representations for initializer_list. initializer_list is used to implement initializer lists as specified in 11.6.4. Copying an initializer list does not copy the underlying elements. —end note ]
That is the reason why it is not legal to declare a constexpr initializer_list
object.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With