Regarding decltype() for captured entities, which compiler is ISO compliant?

122 views Asked by At
#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?

2

There are 2 answers

0
Brian Bi On BEST ANSWER

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&.

2
HolyBlackCat On

Clang is correct in both cases.


First of all, both snippets should print ?01, i.e. std::is_same_v<decltype((j)), const int &> must be true in both.

The lambda is not mutable, meaning its operator() is const, hence any by-value captures appear to be const inside of its body (j = 42; inside of the lambda is a compilation error).


Now, regarding decltype(j). decltype has custom rules for unparenthesized variable names, which makes it return the type specified in the variable declaration, even if we're inside of a const member function.

When a capture-by-copy doesn't have an initializer (possibly because it's implicit), [expr.prim.lambda.capture]/11 kicks in:

Every id-expression within the compound-statement of a lambda-expression that is an odr-use ([basic.def.odr]) of an entity captured by copy is transformed into an access to the corresponding unnamed data member of the closure type.

[Note 7: An id-expression that is not an odr-use refers to the original entity, never to a member of the closure type. However, such an id-expression can still cause the implicit capture of the entity. — end note]

This means that decltype(j) (which is not an ODR-use) in the second snippet refers to the variable in the enclosing function, and returns the type used in its declaration, which is int.

As for the first snippet, expr.prim.lambda.capture]/6 says (in a rather convoluted way) that captures with initializers are behave as if they created a named variable, so decltype(j) has to refer to that generated variable and not the one in the enclosing function.

The generated variable has type int (not int &, since we're capturing by copy), so this is what decltype(j) should return for the first snippet.