问题
So I was cooking up an answer here and I needed to use C++14's identifier initializer within a lambda capture:
const auto cmp = [ordering = { "dog", "cat", "mouse", "elephant" }](const string& lhs, const string& rhs) { return find(cbegin(ordering), cend(ordering), lhs) < find(cbegin(ordering), cend(ordering), rhs); };
And this works fine as long as ordering
is an intializer_list<const char*>
. But for some reason everything falls apart if I make it an intializer_list<string>
:
const auto cmp = [ordering = { "dog"s, "cat"s, "mouse"s, "elephant"s }](const string& lhs, const string& rhs) { return find(cbegin(ordering), cend(ordering), lhs) < find(cbegin(ordering), cend(ordering), rhs); };
Live Example
I would say this was a compiler bug, but I'm seeing an even weirder issue with Visual Studio where everything compares equal when using the intializer_list<string>
, but everything again works fine with an intializer_list<const char*>
, you can copy the test code to http://webcompiler.cloudapp.net to see for yourself.
Is this actually a bug in gcc and Visual Studio or have I done something wrong?
EDIT:
Given code that will use one of these definitions of cmp
:
map<string, int, function<bool(const string&, const string&)>> myMap(cmp);
myMap["cat"s] = 1;
myMap["dog"s] = 2;
myMap["elephant"s] = 3;
myMap["mouse"s] = 4;
myMap["rhino"s] = 5;
for (auto& i : myMap) {
cout << i.first << ' ' << i.second << endl;
}
On Visual Studio and gcc using the intializer_list<const char*>
version correctly generates:
dog 2
cat 1
mouse 4
elephant 3
rhino 5
Using the intializer_list<string>
on http://ideone.com incorrectly generates:
cat 1
dog 2
mouse 4
elephant 5
And using the intializer_list<string>
on Visual Studio incorrectly generates:
cat 5
回答1:
The bug is in your code, I'm afraid.
The problem is that you're copying/moving the lambda when you initialize the cmp
object, which means that the you lose lifetime extension on the array backing the captured initalizer_list
. If you lifetime-extend the lambda by reference binding then your code will work at least for the versions of gcc on ideone and coliru:
const auto& cmp = [ordering = { "dog"s, "cat"s, "mouse"s, "elephant"s }](const string& lhs, const string& rhs) { return find(cbegin(ordering), cend(ordering), lhs) < find(cbegin(ordering), cend(ordering), rhs); };
^-- extend lifetime of lambda and thereby captured initializer_list
Note that this will only work if the original lifetime-extended lambda lives for at least as long as everything that uses it. For example, you would not be able to return myMap
from the enclosing function.
Per lifetime of a std::initializer_list return value the temporary array proxied by the initializer_list
is lifetime-extended to the initializer_list
object, the same as binding a reference to a temporary. If we assume that CWG 1695 applies to initializer_list
s the same as it applies to references (and the language in [dcl.init.list] is that initializer_list
lifetime extension behaves "exactly like binding a reference to a temporary") then an initializer_list
init-capture is only valid as long as the enclosing lambda is valid, and will only persist through the enclosing scope if the enclosing lambda is itself lifetime-extended via reference binding.
Note that your code will still not work in clang, because clang does not implement CWG 1695, and nor in MSVC, I assume for the same reason (I tried the example in CWG 1695 and got the same result; I haven't yet found a bug report for MSVC).
Your code with initializer_list<char const*>
works because the backing array for initializer_list
s of primitive (trivial) types can be treated as constant; you're still accessing a dangling reference, but it happens to have the desired value. I'd guess that the version of gcc on coliru is choosing to place the backing array somewhere the destructed string
s can still be accessed with their pre-destruction values; regardless, you're still accessing destructed objects.
来源:https://stackoverflow.com/questions/37621932/construction-of-an-intializer-liststring-in-a-lambda-capture