llvm / llvm-project

The LLVM Project is a collection of modular and reusable compiler and toolchain technologies.
http://llvm.org
Other
28.33k stars 11.7k forks source link

[clang] wrong decltype results for a reference captured by value #95992

Open mmatrosov opened 3 months ago

mmatrosov commented 3 months ago

Consider the following program (https://godbolt.org/z/8EY4G3n68):

int main() {
    int i = 0;
    const int& ir = i;
    auto f = [ir] mutable -> const int& { 
        // ir = 13; error
        static_assert(!std::is_const_v<decltype(ir)>);
        static_assert(std::is_reference_v<decltype(ir)>);
        return ir;
     };
    return &ir == &f();
}

Here ir is a const ref captured by value. It is represented by const int member in the closure object (https://eel.is/c++draft/expr.prim.lambda.capture#10):

For each entity captured by copy, an unnamed non-static data member is declared in the closure type. The type of such a data member is the referenced type if the entity is a reference to an object

(I personally see no reason why const qualifier should not be stripped off during capture, but this is another story)

The line ir = 13 does not compile with the error cannot assign to a variable captured by copy in a non-mutable lambda which clearly tells that the type is not int (and the error is a bit misleading, since the lambda is marked mutable). The return code of the program is zero, which tells that the type is not a reference.

However, both static_asserts compile, saying ir within the closure is a reference, and is non-const. As if they were applied to the outer ir in the body of the function.

llvmbot commented 3 months ago

@llvm/issue-subscribers-clang-frontend

Author: Mikhail Matrosov (mmatrosov)

Consider the following program (https://godbolt.org/z/8EY4G3n68): ``` int main() { int i = 0; const int& ir = i; auto f = [ir] mutable -> const int& { // ir = 13; error static_assert(!std::is_const_v<decltype(ir)>); static_assert(std::is_reference_v<decltype(ir)>); return ir; }; return &ir == &f(); } ``` Here `ir` is a const ref captured by value. It is represented by `const int` member in the closure object (https://eel.is/c++draft/expr.prim.lambda.capture#10): > For each entity captured by copy, an unnamed non-static data member is declared in the closure type. The type of such a data member is the *referenced type* if the entity is a reference to an object (I personally see no reason why `const` qualifier should not be stripped off during capture, but this is another story) The line `ir = 13` does not compile with the error `cannot assign to a variable captured by copy in a non-mutable lambda` which clearly tells that the type is not `int` (and the error is a bit misleading, since the lambda is marked `mutable`). The return code of the program is zero, which tells that the type is not a reference. However, both `static_asserts` compile, saying `ir` within the closure is a reference, and is non-const. As if they were applied to the outer `ir` in the body of the function.
zyn0217 commented 3 months ago

... and is non-const

It is a const reference, see https://en.cppreference.com/w/cpp/types/is_const#Notes:

Notes If T is a reference type then is_const<T>::value is always false. The proper way to check a potentially-reference type for const-ness is to remove the reference: is_const<typename remove_reference<T>::type>.

https://godbolt.org/z/j1Y548xY5

Edit: Use std::remove_reference_t

MitalAshok commented 3 months ago

As if they were applied to the outer ir in the body of the function.

Yes, that should be the expected behaviour. Since ir in decltype(ir) is an unevaluated context, it refers to the local entity declared in main, not the member of the lambda object. This matches the example given here: https://eel.is/c++draft/expr.prim.id.unqual#example-1

https://eel.is/c++draft/expr.prim.id.unqual#3.sentence-3

If naming the entity from outside of an unevaluated operand within S would refer to an entity captured by copy in some intervening lambda-expression, then let E be the innermost such lambda-expression.

(Bold mine, the transformation shouldn't happen)

mmatrosov commented 3 months ago

Thanks a lot for the links! It is pretty hard for me to get from the text why decltype should behave this way, but it is clear from the example you've referenced that this is indeed the correct behavior. Even though it looks quite surprizing to me. Do I understand correctly that when I write evaluated ir in the lambda body (e.g. assign to it) it refers to the implicit data member of the closure, while when I write unevaluated ir (e.g. apply decltype on it) it refers to the original object outside of the lambda?