According to cppreference.com, std::initializer_lists have constexpr constructors and constexpr size methods (since C++14).
Although the compiler I was using seemed to agree that the size of a constexpr initializer list was indeed constexpr, in some circumstances it didn't believe my list was constexpr. Since std::initializer_lists can involve some "compiler magic," I began wondering whether constexpr doesn't apply to them in quite the same way as it does for non-magical objects.
I hopped onto Compiler Explorer and discovered that the major compilers don't agree on the topic.
So what are the correct behaviors (per the standard) for the four cases below?
#include <initializer_list>
using size_type = std::initializer_list<int>::size_type;
template <typename T>
size_type Foo(std::initializer_list<T> const &list) {
return list.size();
}
int main() {
// 1. Example based on
// https://en.cppreference.com/w/cpp/utility/initializer_list/size
// gcc: works
// clang: no viable c'tor or deduction guide
// msvc: works
static_assert(std::initializer_list{1, 2, 3}.size() == 3);
// 2. Make a constexpr std::initializer_list<T> with T deduced
// gcc: not constant expression
// clang: no viable c'tor or deduction guide
// msvc: works
constexpr auto the_list = std::initializer_list{1, 2, 3};
// 3. Static assert using constexpr size
// gcc: fails because of above
// clang: fails because of above
// msvc: works
static_assert(the_list.size() == 3);
// 4. Extract the size via a constexpr function
// gcc: fails because of above
// clang: fails because of above
// msvc: expression did not evaluate to a constant
constexpr auto the_list_size = Foo(the_list);
return 0;
}
-std=c++20
(also tested with 13.1)-std=c++20
(also tested with 16.0.0)/std:c++20
I would have expected all four cases to compile. But some compilers rejected some of them, causing me to reconsider my understanding. Are the compilers rejecting correct code (or accepting incorrect code)? Am I inadvertently relying on implementation-defined behavior? Is the standard ambiguous?
The closest existing question I can find is Assigning a initializer_list to std::array, but the details there are specific to std::array, which is not the case in my examples.
std::initializer_list{1, 2, 3}
being invalid is a clang bug (it can't deduce <int>
properly. Replacing it with std::initializer_list<int>{1, 2, 3}
leads to the same behaviour as gcc. Or you can skirt around this with auto the_list = { 1, 2, 3 };
.
The problem with constexpr auto the_list = std::initializer_list<int>{1, 2, 3};
is the same problem with constexpr const int& the_number = 123;
. Like with const int&
, std::initializer_list
binds to a temporary (in this case, an array). Also like const int&
, that temporary's lifetime is extended to the lifetime of the initializer_list
variable.
If a constexpr
variable binds to a temporary, that temporary is required to have static storage duration. So, static constexpr auto the_list = std::initializer_list<int>{1, 2, 3};
works (and so does static constexpr const int& the_number = 123;
).
An object of type
std::initializer_list<E>
is constructed from an initializer list as if the implementation generated and materialized ([conv.rval]) a prvalue of type “array of Nconst E
”, [...], and thestd::initializer_list<E>
object is constructed to refer to that array. [...]
The array has the same lifetime as any other temporary object ([class.temporary]), except that initializing an
initializer_list
object from the array extends the lifetime of the array exactly like binding a reference to a temporary.
(e.g., auto the_list = { 1, 2, 3 };
, the const int[3]
has automatic storage duration, but static auto the_list = { 1, 2, 3 };
, the const int[3]
has static storage duration)
An entity is a permitted result of a constant expression if it is an object with static storage duration that either is not a temporary object or is a temporary object whose value satisfies the above constraints, [...]
And your last problem was Foo
not being a constexpr
function. The fixed version:
#include <initializer_list>
using size_type = std::initializer_list<int>::size_type;
template <typename T>
constexpr size_type Foo(std::initializer_list<T> const &list) {
return list.size();
}
int main() {
static_assert(std::initializer_list<int>{1, 2, 3}.size() == 3);
static constexpr auto the_list = {1, 2, 3};
static_assert(the_list.size() == 3);
constexpr auto the_list_size = Foo(the_list);
return 0;
}
compiles on the three given compilers. MSVC was too lenient before about the lifetime of the temporary array.