Why is an initializer_list of enum values not considered a constant expression?

  • A+
Category:Languages

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.)

 


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.

Comment

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: