[cfe-dev] Issue where Lambda capture of reference to globals doesn't actually capture anything

Hollman, Daisy Sophia via cfe-dev cfe-dev at lists.llvm.org
Tue Oct 27 13:50:10 PDT 2020


Yeah, but if you’re explicit about the constexpr it works fine in both compilers: https://godbolt.org/z/xdad8v. Here’s a summary of frontend divergence on this issue: https://godbolt.org/z/KfdKcT. I think basically this is one of those areas where implementation divergence is so significant that no one can write portable code dependent on it, so we should consider simplifying. I understand the desire to make []{ return x; }, [=]{ return x; }, and [x]{ return x; } all do the same thing (though that doesn’t save us from the fact that [x=x]{ return x; } does something different), but the reality is that anyone who is writing code that’s dependent on this behavior is either only using Clang or—worse—depending on the wrong behavior and avoiding Clang. And that doesn’t even always work; consider this example: https://godbolt.org/z/ETMGEq where adding a default capture causes Clang to treat the reference to a global as a reference to const.

I’m not sure where the references-are-implicitly-constexpr part came from

I think this is part of the problem. I think most people would find this at least slightly less confusing:

int foo = 1;
int main() {
  constexpr auto& myfoo = foo;
  auto x = [=]{ return myfoo; };
  std::cout << x() << std::endl;
  foo = 2;
  std::cout << x() << std::endl;
}


But I think probably the best solution would be to consider deprecating the use of constexpr references in capture-by-value lambdas. The argument in many cases is pretty similar to the problems with implicit this capture. Very few people would be surprised by the behavior of this program, for instance:

int foo = 1;
int main() {
  constexpr auto& myfoo = foo;
  auto x = [=, &myfoo]{ return myfoo; };
  std::cout << x() << std::endl;
  foo = 2;
  std::cout << x() << std::endl;
}


The fact that this works the same way currently on all of the major implementations is another bonus.

But this is really the wrong place to have such discussions :)

Yeah, I agree that whatever happens, I think this should probably be a Core issue that we should switch this discussion over to the ISO mailing list.


Thanks,

Daisy 🌼


On October 27, 2020 at 1:00:31 PM, Richard Smith (richard at metafoo.co.uk<mailto:richard at metafoo.co.uk>) wrote:

On Tue, 27 Oct 2020 at 12:11, Arthur O'Dwyer <arthur.j.odwyer at gmail.com<mailto:arthur.j.odwyer at gmail.com>> wrote:
On Mon, Oct 26, 2020 at 8:14 PM Richard Smith via cfe-dev <cfe-dev at lists.llvm.org<mailto:cfe-dev at lists.llvm.org>> wrote:
> On Mon, 26 Oct 2020 at 16:57, Lewis, Cannada via cfe-dev <cfe-dev at lists.llvm.org<mailto:cfe-dev at lists.llvm.org>> wrote:
>>
>> I’m not a standards reading expert but does
>>
>> 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
>>
>> From the text imply that the program https://godbolt.org/z/feKxdK is actually implementation defined? Or does gcc have a bug here?
>
> GCC has a bug, according to the standard wording. The mention of myfoo does not constitute an odr-use, so is not rewritten to use the capture. Clang's behavior is correct per the standard wording.
>
> The standard rule is certainly surprising in this particular case. I think the rule in question is driven by the desire for adding a capture-default to a valid lambda to not change its meaning. For example: https://godbolt.org/z/nrWsvj

Hmm. It's insane that you can use local variable `x` inside a lambda that doesn't capture anything; I wasn't aware of that.

Aside: "insane" (here) and "crazy" (below) are somewhat more charged language than we'd prefer here.

If the formal wording really says you can do that, then that seems like a formal-wording bug to me.

It's entirely intentional, though the case that was being thought about at the time was primarily local constant non-references, not local references (though the same rules apply to both).

void f() {
  constexpr int k = 5;
  [] { int arr[k]; };
}

... should obviously be valid. And more broadly, the rule is that anything that you can name without an odr-use is valid. And this isn't at all special to lambdas; the same thing happens with all nested constructs:

int n;
void g() {
  const int q = 6;
  int &r = n;
  constexpr int *p = &n;
  struct A {
    void f() {
      int arr[q]; // ok
      r = *p + 1; // ok
    }
  };
}

https://godbolt.org/z/avG7qx

GCC and Clang disagree in a different way about whether it's okay to dereference a pointer inside a lambda without capturing it; here it's GCC that is doing the crazy thing, and Clang that is reasonably refusing to compile a reference to `x` when `x` hasn't been captured.
https://godbolt.org/z/bjbr3f

Yeah, GCC's behavior is not in line with the language rules in the pointer case. I'd guess they're using a more-liberal evaluation mode when evaluating the initializer of the reference, rather than using the strict constant initializer rules, and that more-liberal mode permits them to read the initializer values of all const-qualified variables, not only the ones that are usable in constant expressions. (Clang has a more-liberal mode too, and I'd expect we also have bugs where a conforming program can tell the difference.)

The rules in question are approximately equivalent to: if a variable of const integral or enumeration type, or of reference type, can be constexpr, then it is implicitly constexpr. And constexpr variables can be used from anywhere, without triggering an odr-use, if only their value, not their address, is used. (We don't actually say such things are implicitly constexpr, and there are corner cases where the outcome is different -- in particular, for static data members, where constexpr implies inline but const does not. But "implicitly constexpr" is how they behave.)

If you change your example to declare the pointer to be constexpr then the two compilers agree again.

The rules about when compilers are able to "constant-fold" away variables that otherwise would need to be captured, strikes me as similar in spirit to the rules about when compilers are able to "constant-fold" away complicated expressions in constant expressions. Some compilers seem to be trying to be "helpful" by letting the optimizer's knowledge leak into the front-end, and some are following the same rules as one's "head compiler" would.

I think this is at least not the proximal cause in the standard-conforming case -- correctly implementing the odr-use rules in the Clang frontend was a non-trivial effort and wasn't related to constant folding / optimizer knowledge leaking through. But I do think the standard rules here probably originally came from looking at what implementations at the time happened to do and writing that down (eg, use of a const static data member with no definition is OK, because implementations happen to not emit a reference to the symbol), and complexity has built up from that point. I'm not sure where the references-are-implicitly-constexpr part came from, but the const-ints-are-implicitly-constexpr part is a backwards-compatibility concern inherited from C++98.

From a language design perspective, I think it would have made a lot more sense if we'd said that constexpr variables are implicitly static (so the question of capture or of use from other scopes can be answered trivially), and deprecated the "implicit constexpr" behavior for const integral/enumeration variables and references. Where we've ended up isn't a great compromise. But this is really the wrong place to have such discussions :)
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.llvm.org/pipermail/cfe-dev/attachments/20201027/f891a8aa/attachment-0001.html>


More information about the cfe-dev mailing list