Consider this C++11 code snippet:
#include <iostream>
#include <set>
#include <stdexcept>
#include <initializer_list>
int main(int argc, char ** argv)
{
enum Switch {
Switch_1,
Switch_2,
Switch_3,
Switch_XXXX,
};
int foo_1 = 1;
int foo_2 = 2;
int foo_3 = 3;
int foo_4 = 4;
int foo_5 = 5;
int foo_6 = 6;
int foo_7 = 7;
auto get_foos = [=] (Switch ss) -> std::initializer_list<int> {
switch (ss) {
case Switch_1:
return {foo_1, foo_2, foo_3};
case Switch_2:
return {foo_4, foo_5};
case Switch_3:
return {foo_6, foo_7};
default:
throw std::logic_error("invalid switch");
}
};
std::set<int> foos = get_foos(Switch_1);
for (auto && foo : foos) {
std::cout << foo << " ";
}
std::cout << std::endl;
return 0;
}
Whatever compiler I try, all seem to handle it incorrectly. This makes me think that I am doing something wrong rather than it's a common bug across multiple compilers.
clang 3.5 output:
-1078533848 -1078533752 134518134
gcc 4.8.2 output:
-1078845996 -1078845984 3
gcc 4.8.3 output (compiled on http://www.tutorialspoint.com):
1 2 267998238
gcc (unknown version) output (compiled on http://coliru.stacked-crooked.com)
-1785083736 0 6297428
The problem seems to be caused by using std::initializer_list<int>
as a return value of lambda. When changing lambda definition to [=] (Switch ss) -> std::set<int> {...}
returned values are correct.
Please, help me solve this mystery.
The problem is that you are referencing an object that no longer exists and therefore you are invoking undefined behavior.
initializer_list
seems underspecified in the C++11 draft standard, there are no normative sections that actually specify this behavior. Although there are plenty of notes that indicate this will not work and in general although notes are not normative if they don't conflict with the normative text they are strongly indicative.If we go to section
18.9
Initializer lists it has a note which says:and in section
8.5.4
we have the following examples:with the following notes:
These notes are consistent with the initializer_list proposal: N2215 which gives the following example:
and says:
The
initializer_list
in this case just holds pointers to an automatic variable which will not exist after exiting the scope.Update
I just realized the proposal actually points out this misuse scenario:
I find the last statement(emphasis mine) particularly ironic.
Update 2
So defect report 1290 fixes the normative wording and so it now covers this behavior, although the copy case could be more explicit. It says:
The resolution fixes the wording and we can find the new wording in the N3485 version of the draft standard. So section
8.5.4
[dcl.init.list] now says:and
12.2
[class.temporary] says:From: http://en.cppreference.com/w/cpp/utility/initializer_list
I don't think the initializer list is copy-constructable.
std::set
and other containers are. Basically it looks like your code behaves similar to "returning a reference to a temporary".C++14 has something slightly different to say about the underlying storage - extending its lifetime - but that does not fix anything having to do with the lifetime of the
initializer_list
object, let alone copies thereof. Hence, the issue remains, even in C++14.So,
initializer_list
s do not extend the lifetime of their referenced array when they are themselves copied or moved to the result of the copy/move. This makes returning them problematic. (they do extend the lifetime of the referenced array to their own lifetime, but this extension is not transitive over elision or copies of the list).To fix this problem, store the data, and manage its lifetime manually:
the goal here is simple. Create a stack based data type that stores a bunch of
T
s, up to a cap, and can handle having fewer.Now we replace your
std::initializer_list
with:and your code works. The free store is not used (no heap allocation).
A more advanced version would use an array of uninitialized data and manually construct each
T
.