The following program compiles without warnings in GCC and Clang and produces the expected output:
#include <initializer_list>
#include <iostream>
constexpr std::initializer_list<std::initializer_list<const char*>> list = {
{"a", "b", "c"},
{"d"}
};
int main() {
for (const auto& outer: list) {
std::cout << "level:\n";
for (const auto& inner: outer) {
std::cout << " " << inner << "\n";
}
}
}
Using MSVC however, the program does not produce any output at all.
Is this program valid according to the C++ standard? Is this a bug in MSVC? If this is not valid C++ then why is there no warning from GCC or Clang? Is there a better way to create a constexpr nested list where the inner list does not have a fixed size?
[dcl.init.list]/6 (from C++20 draft N4860) states that
The array has the same lifetime as any other temporary object (6.7.7), except that initializing an initializer_list object from the array extends the lifetime of the array exactly like binding a reference to a temporary.
They include these examples:
void f() {
std::vector<cmplx> v2{ 1, 2, 3 };
std::initializer_list<int> i3 = { 1, 2, 3 };
}
struct A {
std::initializer_list<int> i4;
A() : i4{ 1, 2, 3 } {} // ill-formed, would create a dangling reference
};
The standard continues, in the same paragraph, with
For v1 and v2, the initializer_list object is a parameter in a function call, so the array created for { 1, 2, 3 } has full-expression lifetime. For i3, the initializer_list object is a variable, so the array persists for the lifetime of the variable. For i4, the initializer_list object is initialized in the constructor's ctor-initializer as if by binding a temporary array to a reference member, so the program is ill-formed (11.10.2).
(emphasis mine)
In your example, I believe, it is equivalent to example i3; thus the use of the initializer is valid. However, the constexpr, I think, is what is causing the issue. If you remove constexpr, MSVC, g++, and clang are all happy and execute the code.
I might be wrong, but I'd actually think this is a bug in MSVC. When running the code, MSVC exits with STATUS_ACCESS_VIOLATION. I'm assuming this is because the addresses are no longer valid that it's trying to reference when printing -- using int instead of const char* consistently printed 0 for me, though I'd expect that to be more random like access uninitialized memory.
Is this program valid according to the C++ standard? Is this a bug in MSVC?
This should be a bug in MSVC. As indicated by ChrisMM, the lifetime of the backing array is extended to match that of the bound std::initializer_list, similar to how a temporary binds to a reference. Consequently, we can argue that the backing arrays (including nested arrays) of list in your code have the same lifetime as the latter. Regarding whether variables of type initializer_list can be declared with constexpr, although not explicitly stated in the current standard, the intent is clear since its member functions are marked as constexpr, and Clang/GCC have implemented this correctly. Even if MSVC decided not to support this, simply rejecting the code is a more robust behavior than producing broken code, in my opinion.
For a specific example, consider:
#include <initializer_list>
template <class T> using L = std::initializer_list<T>;
constexpr L<int> l0 = {0, 1, 2, 3};
constexpr L<L<int>> l1 = {{0, 1}, {2, 3}};
The relevant part of the assembly code produced by MSVC v19.38 is (Godbolt):
int const * const $S1
DD 00H
DD 01H
DD 02H
DD 03H
std::initializer_list<int> const * const $S2
DQ 0000000000000000H
DQ 0000000000000008H
DQ 0000000000000000H
DQ 0000000000000008H
std::initializer_list<int> const l0
DQ FLAT:int const * const $S1
DQ FLAT:int const * const $S1+16
std::initializer_list<std::initializer_list<int>> const l1
DQ FLAT:std::initializer_list<int> const * const $S2
DQ FLAT:std::initializer_list<int> const * const $S2+32
As you can see, the data of l0 is correct, while the nested initializer_lists in l1 simply hold nullptrs (0000000000000000H), which is why you don't see any output from the program. It's an obvious inconsistency for MSVC to handle l0 and l1 differently. Another inconsistency is that if we change constexpr to constinit (which has similar effects to the former in this case) in the above code, MSVC rejects both initializations of l0 and l1 (Godbolt).
Is there a better way to create a
constexprnested list where the inner list does not have a fixed size?
I would say there is no better way than using nested initializer_lists since it has language-level support and is clean and elegant. As a workaround, you may simply drop the constexpr keyword until MSVC fixes this issue. For two-dimensional lists (also known as jagged arrays), the following type may serve as an alternative (Godbolt):
template <class T, size_t... Ns> class JaggedArray {
public:
constexpr JaggedArray(array<T, Ns> &&...arrays) noexcept
: arrays{std::move(arrays)...} {
[&]<size_t... Is>(index_sequence<Is...>) {
((spans[Is] = get<Is>(this->arrays)), ...);
}(make_index_sequence<sizeof...(Ns)>());
}
constexpr auto begin() const noexcept { return spans.begin(); }
constexpr auto end() const noexcept { return spans.end(); }
private:
tuple<array<T, Ns>...> arrays;
array<span<const T>, sizeof...(Ns)> spans;
};
template <class T, size_t... Ns>
JaggedArray(T (&&...arrays)[Ns]) -> JaggedArray<T, Ns...>;
This still has limitations, as it doesn't support non-movable types. Note that the workaround mentioned by Marek doesn't seem to work either. Using the same example above, modify:
constexpr L<L<int>> l1 = {L<int>{0, 1}, L<int>{2, 3}};
MSVC produces (Godbolt):
int const * const $S5
DD 02H DUP (?)
int const * const $S6
DD 02H DUP (?)
std::initializer_list<int> const * const $S4
DQ FLAT:int const * const $S5
DQ FLAT:int const * const $S5+8
DQ FLAT:int const * const $S6
DQ FLAT:int const * const $S6+8
std::initializer_list<std::initializer_list<int>> const l1
DQ FLAT:std::initializer_list<int> const * const $S4
DQ FLAT:std::initializer_list<int> const * const $S4+32
The good news is that this time there are no nullptrs, but the bad news is that the referenced storage (i.e. S5 and S6) didn't get initialized (note the (?) part) to the desired values.
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