Look at this program (godbolt):
template <typename> struct TD;
void foo(int &par) {
auto l = [par]() {
TD<decltype(par)>();
};
}
It intentionally doesn't compile, it uses a technique to make the compiler to display the type of par. Note that par is a by-value capture.
What is the type of par inside the lambda? Both gcc and clang give an error message which tells that par's type is int &:
<source>:5:9: error: implicit instantiation of undefined template 'TD<int &>'
On the other hand, MSVC complains about TD<int>:
<source>(5): error C2027: use of undefined type 'TD<int>'
If I change the standard to C++20 to MSVC, it also complains about int &.
The current draft standard says: For each entity captured by copy, an unnamed non-static data member is declared in the closure type. The declaration order of these members is unspecified. The type of such a data member is the referenced type if the entity is a reference to an object, an lvalue reference to the referenced function type if the entity is a reference to a function, or the type of the corresponding captured entity otherwise. A member of an anonymous union shall not be captured by copy.
Which compiler is correct? Have something related changed in the standard? If par really should be reference according to the standard, then why? As far as I understand the cited text, it supposed to be the referenced type, which is just int.
(Note: printing out par's address in foo and in the lambda, they differ, so the compiler did make a copy. But then why is par in the lambda a reference? Did the compiler make an anonymous copy ofpar and then bind it to a non-const reference?)
The correct type is int&, as explained in [dcl.type.decltype] p1.
otherwise, if
Eis an unparenthesized id-expression or [...],decltype(E)is the type of the entity named byE. If there is no such entity, the program is ill-formed;
- [dcl.type.decltype] p1.3
par is an unparenthesized id-expression, and the type of the par entity is int&. It just remains to be shown that name lookup for the unqualified-id par finds the function parameter, not the captured variable in the lambda.
Name lookup does find the int& par parameter due to the rules in [expr.prim.id.unqual] p3, which have the following example:
float f() { float x, &r = x; [=]() -> decltype((x)) { // ... decltype(r) r1 = y1; // r1 has type float& // ... } }
Even though par (or r in the standard example) is captured by copy, name lookup in an unevaluated operand does not find it.
A more detailed explanation of these rules can be found at regarding the type derivation of lambda-captured variables, which is iso compliant
Also, If you wrote an init-capture [par = par], the rules would apply again, and decltype would find the captured par.
Note: The C++20 wording for [expr.prim.id.unqual] may be easier to understand. This was changed in C++23 through p2036 Change scope of lambda trailing-return-type
All compilers actually agree on this, but MSVC deviates in /permissive mode.
This is the default, so you're getting this surprising behavior.
/std:c++20 disables MSVC compiler extensions, so it is as if you had written /permissive-. Adding the /permissive- flag on its own also makes MSVC think that the type is int&.
You can find an example of this feature in Microsoft's documentation:
decltype(auto) x = cond ? a : b; // char without, const char& with /Zc:ternary
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With