C++ Insights new lambda transformation
Last week I pushed an updated lambda transformation for C++ Insights. Here is a little background on the change.
Since I first published C++ Insights when you transformed a lambda like the following:
1 2 3 4 5 6 | |
The result looked like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | |
Now, what's wrong with this transformation? Well, there is no constructor in a lambda A. Never. Except for a default constructor in case of a captureless lambda.
But, I needed the constructor to initialize the data member val because the standard also requires that all captures become private data members. More importantly, the standard also requires that all copy-captured variables are direct-initialized: [expr.prim.lambda.capture] p15
When the lambda-expression is evaluated, the entities that are captured by copy are used to direct-initialize each corresponding non-static data member
Now, this direct-initialization was something that the earlier transformation didn't show. Instead, you saw the constructor, which aimed to initialize things as good as possible. I could argue that for many cases there was no difference.
However, there was a difference. Consider this example, which is derived from Issue2.cpp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | |
We have a move-only object that is copy-captured into a lambda. The old transformation produced the following code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | |
I had to move the object during the constructor call B and a second time in the constructor initializer list A. This isn't as good as the code your compiler generates for you! This is when direct initialization makes a difference.
Imagine the object Movable would contain a data member like this:
1 2 3 4 5 6 7 8 9 10 11 | |
The move-constructor of Moveable would of course move the std::string data member text A. But it would be moved twice as well. Now in your std::string object there is more data than just the pointer to the string data. A typical std::string store the currently allocated capacity and the length of the string. Let's say to size_t. They can't be moved. They are always copies. This is where the former transformation presented you with bad code.
Aside from that, I had to insert the header utility for std::move.
What do I change? Well, for a long time I was checking how compilers solve this. One way, of course, would be to rename the data-members inside the lambda. This would be fine under the umbrella of the standard. It specifically calls this out in [expr.prim.lambda.capture] p10
For each entity captured by copy, an unnamed non-static data member is declared in the closure type.
The drawback with this approach is that I have to track all the captures and rename all their uses. Which would obfuscate the name as well.
The trick that at least GCC seems to do is to start a lambda as a struct with the captures as public data members. Then it uses aggregate initialization to directly initialize all the captures. After this step, the compiler internally flips the struct into a class. But that it fulfills the requirements of the standard but can also use standard code generation for the direct-init case.
Now, I can't do the flip. So I decided to violate the standard in a way that I make the data members public. I only leave the public as a comment. While still a violation, it seems the better one than before.
With the new transformation, the generated code now looks like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | |
You can see the commented private in A with the constructor gone. Only the std::move we as users must write is there.
The motivation for this change came after another chat with Jason Turner at CppCon 2024. We had that discussion a couple of times. That day I sat down later that night and implemented the change. After something that nearly tuned into an all-nighter, I was compelled to think that this was the better way to go.
The change dropped over 500 lines of code and reduced. I also could remove a couple of special treatments, and the test output became shorter as well.
Overall, I think this change is a great improvement to C++ Insights. I hope this is an improvement for you to.
Please don't hesitate to reach out and let me know your thoughts!
Support the project
You can support the project by becoming a GitHub Sponsor, or Patreon or, of course, with code contributions.
As a company you can sponsor C++ Insights and get your company logo shown in the top banner. Please reach out, if you want to discuss this option.
Andreas