#include <iostream>
#include <type_traits>
int main(){
int i = 1;
int& j = i;
auto f2 = [j = j]() {
std::cout
<< std::is_same_v<decltype(j), int&>
<< std::is_same_v<decltype((j)), int&>
<< std::is_same_v<decltype((j)), const int&>;
};
auto f3 = [=]() {
std::cout
<< std::is_same_v<decltype(j), int&>
<< std::is_same_v<decltype((j)), int&>
<< std::is_same_v<decltype((j)), const int&>;
};
f2();
f3();
}
The following output is the result of the c++17 standard
gcc | clang | msvc |
---|---|---|
001 | 001 | 010 |
110 | 101 | 101 |
Or all the compilers are wrong?
Clang is correct; the results given by Clang conform to the wording introduced by P0588R1, which was accepted after the publication of C++17 but has DR status, meaning that it is recommended that implementations treat the original specification prior to P0588R1 as having been incorrectly drafted, and that the rules in P0588R1 should be applied by implementations even in C++17 mode and earlier.
The following example from P0588R1 illustrates the application of the rules to an example very similar to your second example, f3
:
void f() {
float x, &r = x;
[=] {
decltype(x) y1; // y1 has type float
decltype((x)) y2 = y1; // y2 has type float const& because this lambda is not mutable and x is an lvalue
decltype(r) r1 = y1; // r1 has type float&
decltype((r)) r2 = y2; // r2 has type float const&
};
}
In your example, you have the reference j
instead of the reference r
, but otherwise, it's the same. So decltype(j)
should be int&
, while decltype((j))
should be const int&
.
To elaborate, because decltype(j)
and decltype((j))
are not odr-uses of j
, they denote the original entity from the enclosing scope. (There is no copy of j
stored inside the lambda, because the lambda did not odr-use j
; but even if the lambda had odr-used j
and thus created a copy of it, non-odr-uses of j
will not refer to that copy.) However, confusingly, the type of j
inside the lambda is not necessarily the type of the entity that j
denotes. Instead, to determine the type of j
, we consider a hypothetical odr-use of j
; then, we take the type from the type of a hypothetical member access expression to that hypothetical copy.
In the case of decltype(j)
, because j
is not parenthesized, decltype
ignores the type of the expression, and gives the declared type of the denoted entity; this is int&
, the declared type of the variable j
. In decltype((j))
, because j
is parenthesized, decltype
ignores the declared type of j
, and looks at the type and value category of the expression j
. Now, the hypothetical odr-use of j
would result in an int
member being declared in the closure type, but a hypothetical member access expression to that member would give an lvalue of type const int
, because the lambda is not mutable. Consequently, decltype((j))
is const int&
.
Now, let's apply these rules to the case of f2
. Here's where it gets a bit tricky because you have an init-capture. The init-capture actually introduces a new name j
that shadows the j
belonging to main
. That new name denotes a member of the closure type, whose type is determined as if you had written
auto j = j;
except that the first j
's scope doesn't begin until after the closing square bracket of the lambda-introducer, so the second j
refers to the j
that belongs to main
.
Now, any mention of j
in the lambda body, whether an odr-use or not, denotes the j
declared by the init-capture. That j
has declared type int
, so decltype(j)
is int
. For decltype((j))
, we again consider a member access expression that would denote the member j
; that member access expression would again be an lvalue of type const int
, so decltype((j))
is const int&
.