2

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

Community
  • 1
  • 1
Jonathan Mee
  • 37,899
  • 23
  • 129
  • 288
  • 3
    What if you make `ordering` a local variable of the lambda's body? Looks like it doesn't need to capture anything external to the lambda there. – KABoissonneault Jun 03 '16 at 19:25
  • 1
    What version of Visual Studio are you using? It compiles fine for me in 2015, but not in 2013. IIRC 2013 doesn't have complete C++14 support, so maybe you are using features that aren't supposed to work. – Andy Jun 03 '16 at 19:27
  • @Andy I didn't suggest it didn't compile. Copy the code from my gcc example into Visual Studio 2015. Note the bad results that you get when using the `intializer_list`. – Jonathan Mee Jun 03 '16 at 19:33
  • @KABoissonneault I'm not looking for an alternate implementation it already works fine as an `intializer_list`. I'm trying to figure out why `intializer_list` doesn't work. – Jonathan Mee Jun 03 '16 at 19:35
  • 1
    It works as expected on Coliru's GCC and Clang. – chris Jun 03 '16 at 19:36
  • @chris Oh snap so it does: http://coliru.stacked-crooked.com/a/3f0a1be1f89123fe I need to find the version difference between the gcc on http://coliru.stacked-crooked.com and http://ideone.com – Jonathan Mee Jun 03 '16 at 19:39
  • 1
    @JonathanMee, Try [Wandbox](http://melpon.org/wandbox) to pin down the version. – chris Jun 03 '16 at 19:44
  • @chris Well this is freaky. I can't get the same error on http://melpon.org/wandbox that I'm getting on http://ideone.com whadaya make of that? – Jonathan Mee Jun 03 '16 at 19:49
  • Can you provide a [mcve]? I have no idea what you're claiming is wrong. Define "falls apart." – Barry Jun 03 '16 at 19:58
  • 1
    @JonathanMee, Strange, I'm guessing this error occurs in a small set of GCC versions between this example compiling and it giving correct output. AFAICT, it only compiles since about GCC 5. Perhaps ideone is using 5.0 (GCC has started making x.1 the first release of a new major version), which I guess would be a bit less production-ready. – chris Jun 03 '16 at 20:01
  • @Barry Sorry I had an MCVE, but it was just in the Live Example link. It is clearer to have the code and the results in the question, so I have edited. – Jonathan Mee Jun 03 '16 at 20:22
  • 1
    Where do you think that `initializer_list`'s backing array's lifetime ends? – T.C. Jun 03 '16 at 21:42
  • @T.C. Ummm... I think it's lifetime works how the link I posted the question says: "A capture with an initializer acts as if it declares and explicitly captures a variable declared with type auto, whose declarative region is the body of the lambda expression (that is, it is not in scope within its initializer)" So the `intializer_list` exists as a member of the lambda which will be destroyed when the lambda is destroyed. Are you questioning that? – Jonathan Mee Jun 03 '16 at 21:52
  • I'm not talking about the `initializer_list`, which is just a pair of pointers. I'm talking about the array backing it. – T.C. Jun 03 '16 at 22:00

1 Answers1

4

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_lists 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_lists 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 strings can still be accessed with their pre-destruction values; regardless, you're still accessing destructed objects.

Community
  • 1
  • 1
ecatmur
  • 152,476
  • 27
  • 293
  • 366
  • Hah, I actually missed that elided copy (but, but, P0135R0 ;)). Looks like GCC does do lifetime extension with `auto&&` or `const auto&`. – T.C. Jun 03 '16 at 22:29
  • 1
    Depends on the final wording, I suppose. [Richard Smith's current draft](https://rawgit.com/zygoloid/wg21papers/master/wip/d0135r1.html) says it would resolve that issue, but I haven't read it in detail yet to figure out in which manner. – T.C. Jun 03 '16 at 22:33
  • @T.C. I'm gonna need some help here, I thought that an = in the declaration line just used a constructor, for example: `const auto foo = vector(13);` should just construct a `vector`. Are you suggesting that this is going to construct and then move? – Jonathan Mee Jun 03 '16 at 22:41
  • 1
    @JonathanMee it constructs and moves; in practice, the move is elided, but the elided move means that (a) you have to have a copy/move constructor available, even though it isn't actually called, and (b) you lose lifetime extension on captures. As T.C. says, P0135R0 looks set to fix this, hopefully for C++17. – ecatmur Jun 03 '16 at 22:48
  • 1
    I think I'll need to sleep on that wording, but currently I'm fairly sure that those rules will result in identical behavior with or without the `&`, so presumably extension in both cases. @JonathanMee Yes, that's currently a construct + a elidable move. You can see it yourself with `-fno-elide-constructors`. – T.C. Jun 03 '16 at 22:50
  • @T.C. that's a great read, thanks! I suppose the magic wording will be the new bullet "*If the initializer expression is a prvalue and the cv-unqualified version of the source type is the same class as the class of the destination, the initializer expression is used to initialize the destination object. [...]*" I'm still a little uncertain what "used to initialize" means, but the intent at least is clear. – ecatmur Jun 03 '16 at 22:57
  • @T.C. Wow, my whole life has been a lie: http://ideone.com/Uj7o3q So I've probably misunderstood this too, but I can return an `intializer_list` that was created within the scope of a function can't I? I mean provided the answer to that is yes, the capture should copy the `intializer_list` to make it part of the lambda object. – Jonathan Mee Jun 03 '16 at 22:57
  • 2
    @JonathanMee No, it's as bad as returning a reference to a local variable. – T.C. Jun 03 '16 at 22:59
  • @T.C. Man, I thought `intializer_list` was a lightweight container. So now I'm really confused. `const auto& cmp`? How is that even gonna compile? How's that not just as bad as `const auto& foo = 13;`? Isn't the lambda object just going to get destroyed at the synchronization point? – Jonathan Mee Jun 03 '16 at 23:05
  • 1
    @Jonathan Mee it's called "lifetime extension by reference binding" and it's a pretty dark corner of the language. Indeed, `const auto& foo = 13;` is valid and lifetime-extends a temporary `int` for the rest of the scope. – ecatmur Jun 03 '16 at 23:09
  • @T.C. I feel like I must have tagger this question with something other than C++ cause you guys are blowing my mind. So you're saying that elided move construction will deallocate the memory backing the `initializer_list`, but this "lifetime extension by reference binding" will keep the memory from being deallocated? – Jonathan Mee Jun 04 '16 at 03:02
  • @JonathanMee Assuming that CWG 1695's conclusion is applicable here, the backing array of `string`s (or `const char*`s) is alive until the *initial* `initializer_list` object dies, which in turn is alive until the *initial* closure object dies. Reference binding extends the lifetime of that closure object, thus extending the lifetime of the `initializer_list` object, which in turn extends the lifetime of the backing array. – T.C. Jun 04 '16 at 05:42
  • @T.C. OK, so last question, then I'll accept this answer: In the linked answer I mention constructing a lambda temporary *in* the constructor of the `map`. So how are we saying that would work? Since the comparison parameter is pass by reference, does that mean the backing for the `initializer_list` would last as long as the `map` did, or as long as the scope the `map` was constructed in did? The latter? – Jonathan Mee Jun 04 '16 at 11:57
  • @JonathanMee it wouldn't work, except by accident. Lifetime extension by reference binding is very fragile, and certainly can't survive being passed through a user-defined constructor. – ecatmur Jun 04 '16 at 13:31
  • @JonathanMee No, it only lasts until the end of the full-expression containing the call, and is definitely dead by the next semicolon. – T.C. Jun 04 '16 at 15:13
  • @T.C. I've spun off a couple of questions from here: http://stackoverflow.com/q/37656840/2642059 and http://stackoverflow.com/q/37656076/2642059 just me trying to mull through everything that you guys have taught me here. They're directly related so I thought I'd link them. – Jonathan Mee Jun 06 '16 at 12:15