There he explained very well that he noticed that C++ Insights doesn't show the transformation of structured bindings correctly. He provided the following example:
1 2 3 4 5 6 7
At the time, C++ Insights showed the following transformation:
1 2 3 4 5 6 7 8 9
Dawid noticed that according to the standard ([dcl.dcl] p4), the internally created variable
__tup6 should be moved in this example. Making the result look like this:
1 2 3 4
The example above is also from Dawid. While I totally agreed with what he wrote so far, I immediately reacted with "hell no" to the suggested transformation. I thought that couldn't be true,
__tup6 is after A a moved-from object, and it should not be touched until it was brought back in a known state. This is what I teach all the time, and it is one of the toughest rules when it comes to move semantics. Finding an operation without a precondition to set a moved-from object back into a known state requires careful reading of the objects API. Seeing code like that above automatically turns on all my alarm bells.
Nonetheless, Dawid was absolutely right.
__tup6 is casted to an rvalue reference at this point, or more precisely to an xvalue. I will not go into the details of the different categories here. If you like to know more about the value categories, I recommend reading Dawid's post Value categories – [l, gl, x, r, pr]values. Back to what the compiler does and where C++ Insights was wrong or was it?
The compiler does cast
__tup6 to an xvalue in A and B above, and C++ Insights did show it if you did turn on the extra option "show all implicit casts". This option is off by default because, in my experience, it adds too much noise. The compiler does an incredible amount of casts for us to make even trivial code compile. However, even with all implicit casts on, the transformation C++ Insights showed was incorrect. The compiler knows that the implicit cast is a cast to an xvalue. Hence there is no need to add the
&& to the type. For us, without the
&& the cast is not an xvalue cast. I modified C++ Insights to add the required
&& to the type when the cast is an implicit cast. This corrects more code than just the structured bindings. The second this that C++ Insights does now is to show the implicit xvalue cast in case of structured bindings regardless of the "show all implicit casts" option. In the default mode, "show all implicit casts off", the transformation now produces the following result:
1 2 3 4
Now, we can see the xvalue cast in A and B. Perfect so far, and thank you for Dawid for spotting and reporting this issue.
But why should you care?
Because the above becomes important when you implement your own structured binding decomposition. Have a look at 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 36 37 38 39 40 41 42 43 44 45 46
In A, we create a
struct S with two public data members and apply in-class member initializers. The third one is
private and should not be decomposed. This is the reason why we have to write our own
get function, which we see in B, and provided the required tuple-API in C. This tells the compiler that
S has to data members with type
std::vector<int>. All that looks good.
main, we create an
S object (D) and decompose it into two variables,
b (E). With all that I told you above and looking at the provided code, what do you think about F? This assertion is satisfied, correct? Back at the top in A, we initialized
b with three elements. We are good, right? This is how the
main part looks in the fixed C++ Insights version:
1 2 3 4
Back to the "are we good" question. No, we are not good. The assert in F fires! It does so because of the
static_cast in G and H. This is the
std::move Dawid made me aware of. Have a look at B of the original version of the code. There,
get takes the parameter as an lvalue. But in G, the compiler applies a
__obj43, which leads to a move-construction of
std::vector is a move-aware container, and it does its job. When the compiler passes
__obj43 in G, the first time to
get a new object is created, and
__obj43 is moved into it with the contents of
b! We now do have a moved-from object
__obj43. Hence in the second call to
get in H,
__obj43 has an empty
There are two ways around this, either make
get take a
const S& or
S&&. In both cases, the
std::move-equivalent call from the compiler does not create a new object, so
b remains intact.
The lesson from this never make
get take an lvalue, use
T&& as default, and
const T& as an alternative as long as you do not have a very good reason to fallback to the lvalue.
Support the project