I'm running on Debian unstable with GCC 9.3.0.
There was a recent change on a project I work on that introduced code similar to what's below.
#include <initializer_list>
#include <map>
#include <vector>
std::map<int, std::vector<int>> ex = []{
/* for reused lists */
std::initializer_list<int> module_options;
return (decltype(ex)) {
{1, module_options = {
1, 2, 3
}},
{2, module_options},
};
}();
The idea is that identical subsections of the initializer lists are first declared at the top, defined and assigned to the std:initializer_list
variable at the first usage, then used in multiple places. This is convenient, and some may argue more readable, which is why it was accepted.
All was well until a few days ago where GCC started throwing a init-list-lifetime
warning on the code. We use -Werror
in our regression, so this fails the regression for me. I also tried compiling with clang 9.0.1, which does not throw the warning.
<source>: In lambda function:
<source>:12:9: warning: assignment from temporary 'initializer_list' does not extend the lifetime of the underlying array [-Winit-list-lifetime]
12 | }},
| ^
According to cppreference:
The underlying array is not guaranteed to exist after the lifetime of the original initializer list object has ended. The storage for std::initializer_list is unspecified (i.e. it could be automatic, temporary, or static read-only memory, depending on the situation).
So my understanding is that the common initializer list value, being defined within the scope of an encompassing initializer list, has a lifetime that ends with the enclosing initializer list. From the cppreference page earlier, it mentions that std::initializer_list
is a "lightweight proxy-object", which implies that it does not take ownership of the temporary object or extend it's lifetime. This means that the underlying array is not guaranteed to exist in later usage, which is why the warning is being thrown. Is this analysis correct?
I can prevent the warning from occuring by moving the std::initializer_list
variable initialization to the declaration. For full details on the problem as it stands in the project see the PR.
When there is the potential for this confusion, GCC issues a warning when this flag is specified. To eliminate the warning, add explicit braces around the innermost if statement so there is no way the else can belong to the enclosing if. The resulting code looks like this: This warning is enabled by -Wparentheses .
If the value of y is always 1, 2 or 3, then x is always initialized, but GCC doesn’t know this. To suppress the warning, you need to provide a default case with assert(0) or similar code. This option also warns when a non-volatile automatic variable might be changed by a call to longjmp. The compiler sees only the calls to setjmp.
To help detect accidental misuses of such arrays GCC issues warnings unless it can prove that the use is safe. See Common Variable Attributes . Warn for cases where adding an attribute may be beneficial.
These warnings are only possible in optimizing compilation, because otherwise GCC does not keep track of the state of variables. These warnings are made optional because GCC may not be able to determine when the code is correct in spite of appearing to have an error. Here is one example of how this can happen:
So my understanding is that the common initializer list value, being defined within the scope of an encompassing initializer list, has a lifetime that ends with the enclosing initializer list
You're talking about the object created by the prvalue expression {1, 2, 3}
, right?
There's an example in decl.init.list/6,
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. [Example:
// ...
std::initializer_list<int> i3 = { 1, 2, 3 };
// ...
of which the standard (or draft) says
For
i3
, theinitializer_list
object is a variable, so the array persists for the lifetime of the variable.
This suggests the object must be materialized and should have its lifetime extended.
However, you are not initializing the initializer_list
object from the expression, because your variable is already initialized. If we rewrote your code as a call to a notional
module_options.operator=({1, 2, 3})
then we wouldn't expect the temporary lifetime to be extended past the end of the function call.
I had suspected the temporary would still live to the end of the full-expression, since I thought that binding a reference to was supposed to extend its lifetime rather than reduce it: but admittedly class.temporary/6 says "... persists for the lifetime of the reference ..." and not "... persists for at least the lifetime ..."
However, it does mean that the following variation of your original code should do what you want:
std::map<int, std::vector<int>> ex = []{
/* for reused lists */
std::initializer_list<int> module_options { 1, 2, 3 };
return (decltype(ex)) {
{1, module_options},
{2, module_options},
};
}();
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