This is an archive of the discontinued LLVM Phabricator instance.

[clang-tidy] Aliasing: Add support for lambda captures.
ClosedPublic

Authored by NoQ on Feb 6 2021, 7:29 PM.

Details

Summary

The utility function clang::tidy::utils::hasPtrOrReferenceInFunc() scans the function for pointer/reference aliases to a given variable. It currently scans for operator & over that variable and for declarations of references to that variable.

I'm arguing that it should scan for lambda captures by reference as well. If a lambda captures a variable by reference it basically has a reference to that variable that the user of the lambda can access at any time, including on background thread, which may have meaningful impact on checkers. The same applies to blocks (an Apple extension that brings closures to C and Objective-C; blocks are also implicitly convertible with lambdas when both facilities are available).

The patch fixes false positives in both checkers that use this functionality: bugprone-infinite-loop and bugprone-redundant-branch-condition. There are a few false negatives it introduces as a tradeoff: if the lambda captures a variable by reference but never actually mutates it we'll no longer warn but we should (as demonstrated by FIXME tests). These false negatives are architecturally annoying to deal with because hasPtrOrReferenceInFunc() detects aliasing rather than mutation, so it'll have to be a different facility entirely. They're also rudimentary because there's rarely a good reason to write such code; it probably deserves a separate warning to relax capture kind to a by-value or by-const-reference capture (which is often explicit). That said, C++'s [&] would probably capture a lot of stuff by reference unnecessarily and it'll often make their code worse if developers are forced to write down all captures manually instead.

Diff Detail

Event Timeline

NoQ created this revision.Feb 6 2021, 7:29 PM
NoQ requested review of this revision.Feb 6 2021, 7:29 PM
NoQ edited the summary of this revision. (Show Details)Feb 6 2021, 11:43 PM

Notionally, I think it makes sense to treat byref captures as a form of aliasing. Should structured bindings be treated similarly as well (not necessarily as part of this patch)?

NoQ added a comment.Feb 8 2021, 7:36 PM

Should structured bindings be treated similarly as well (not necessarily as part of this patch)?

Umm, looks like we're both missing the elephant in the room: passing a variable into a function by reference.

int &hidden_reference(int &x) {
  return x;
}

void test_hidden_reference() {
  int x = 0;
  int &y = hidden_reference(x);
  for (; x < 10; ++y) { // Warns ¯\_(ツ)_/¯
  }
}

With this taken care of, I hope structured bindings will be handled automatically because whatever's unwrapped couldn't have obtained a reference to our local variable without going through a function first (eg., the constructor). Unless there's some aggregate magic going on... but in this case, again, we have to take care of aggregate magic and structured bindings aren't at fault.

In D96215#2550227, @NoQ wrote:

Should structured bindings be treated similarly as well (not necessarily as part of this patch)?

Umm, looks like we're both missing the elephant in the room: passing a variable into a function by reference.

Additionally, pointers to non-const objects. I had assumed both of these were intentionally out-of-scope because of flow sensitivity though.

int &hidden_reference(int &x) {
  return x;
}

void test_hidden_reference() {
  int x = 0;
  int &y = hidden_reference(x);
  for (; x < 10; ++y) { // Warns ¯\_(ツ)_/¯
  }
}

With this taken care of, I hope structured bindings will be handled automatically because whatever's unwrapped couldn't have obtained a reference to our local variable without going through a function first (eg., the constructor). Unless there's some aggregate magic going on... but in this case, again, we have to take care of aggregate magic and structured bindings aren't at fault.

FWIW, I was thinking of code like: https://godbolt.org/z/jK53G5 but, I don't think it needs to be handled right now in this patch.

NoQ updated this revision to Diff 342550.May 3 2021, 2:42 PM
NoQ retitled this revision from [clang-tidy] Recognize captures as a form of aliasing. to [clang-tidy] Aliasing: Add support for lambda captures..

Trivial rebase.

Herald added a project: Restricted Project. · View Herald TranscriptMay 3 2021, 2:43 PM
NoQ added a comment.May 3 2021, 3:33 PM

I covered pass-by-reference-into-function in D101790 and decomposition (from a certain point of view) in D101791.

I'm arguing that it should scan for lambda captures by reference as well.

What about immediately invoked lambdas? Would it make sense to exclude those?

Don't mind my previous comment :) I missed the conversation about function by-ref arguments. :)

In D96215#2550227, @NoQ wrote:

Umm, looks like we're both missing the elephant in the room: passing a variable into a function by reference.

int &hidden_reference(int &x) {
  return x;
}

void test_hidden_reference() {
  int x = 0;
  int &y = hidden_reference(x);
  for (; x < 10; ++y) { // Warns ¯\_(ツ)_/¯
  }
}

With this taken care of, I hope structured bindings will be handled automatically because whatever's unwrapped couldn't have obtained a reference to our local variable without going through a function first (eg., the constructor). Unless there's some aggregate magic going on... but in this case, again, we have to take care of aggregate magic and structured bindings aren't at fault.

Just my $0.02, We shouldn't worry too much about pathological cases like that. I imagine that code would almost never appear in the wild. And for cases where you don't have access to definition for hidden_reference , there's not much that can be done anyway.

NoQ updated this revision to Diff 342844.EditedMay 4 2021, 1:18 PM

Unforget to add -fblocks to the bugprone-redundant-branch-condition test. Thanks pre-commit testing! - it wasn't failing on my machine, I guess it's enabled by default on darwin.

NoQ added a comment.May 4 2021, 1:22 PM

Just my $0.02, We shouldn't worry too much about pathological cases like that. I imagine that code would almost never appear in the wild. And for cases where you don't have access to definition for hidden_reference , there's not much that can be done anyway.

That's not what I typically tell myself; it really amazes me that for almost all such pathological cases there appear to be users who write such code on a regular basis.

That said, this specific false positive with references (unlike the two lambda/block false positives in this patch and the next patch) wasn't derived from a bug report *yet*. So i'm not super worried about it.

xazax.hun added inline comments.May 4 2021, 1:58 PM
clang-tools-extra/clang-tidy/utils/Aliasing.cpp
45

Will this cover generalized captures? E.g.:

int i;
auto lambda = [foo=&i] { return *foo; };
NoQ added inline comments.May 4 2021, 3:07 PM
clang-tools-extra/clang-tidy/utils/Aliasing.cpp
45

This one's good; we still react on the operator & inside the init-expression. This one's worse however: [&foo = i] (i had such test for the next patch but not for this patch). Let me patch this up.

In D96215#2737085, @NoQ wrote:

Just my $0.02, We shouldn't worry too much about pathological cases like that. I imagine that code would almost never appear in the wild. And for cases where you don't have access to definition for hidden_reference , there's not much that can be done anyway.

That's not what I typically tell myself; it really amazes me that for almost all such pathological cases there appear to be users who write such code on a regular basis.

+1, Hyrum's Law is real. :-) (or :-(, more accurately).

clang-tools-extra/clang-tidy/utils/Aliasing.cpp
45–48

Should this use capturesByRef() from https://reviews.llvm.org/D101787?

NoQ added inline comments.May 5 2021, 6:34 PM
clang-tools-extra/clang-tidy/utils/Aliasing.cpp
45–48

Yes and https://reviews.llvm.org/D101787 already converts this code to use capturesByRef() as soon as it introduces it!

aaron.ballman accepted this revision.May 6 2021, 4:26 AM

LGTM!

clang-tools-extra/clang-tidy/utils/Aliasing.cpp
45–48

Ah! It wasn't clear to me that's how this patch was stacking up, thanks for the explanation.

This revision is now accepted and ready to land.May 6 2021, 4:26 AM
This revision was landed with ongoing or failed builds.May 10 2021, 2:00 PM
This revision was automatically updated to reflect the committed changes.
NoQ added inline comments.May 10 2021, 7:56 PM
clang-tools-extra/clang-tidy/utils/Aliasing.cpp
45–48

I've been using the phabricator's parent/child revisions feature (aka the Stack tab). I guess i should pronounce such relations out loud because it's not super apparent.