Skip to content

Commit e07681e

Browse files
martinboehmetchaikov
authored andcommitted
[clang-tidy] Let bugprone-use-after-move ignore the moved variable in callee
In C++17, callee is guaranteed to be sequenced before arguments. This eliminates false positives in bugprone-use-after-move where a variable is used in the callee and moved from in the arguments. We introduce one special case: If the callee is a MemberExpr with a DeclRefExpr as its base, we consider it to be sequenced after the arguments. This is because the variable referenced in the base will only actually be accessed when the call happens, i.e. once all of the arguments have been evaluated. This has no basis in the C++ standard, but it reflects actual behavior that is relevant to a use-after-move scenario: ``` a.bar(consumeA(std::move(a)); In this example, we end up accessing a after it has been moved from, even though nominally the callee a.bar is evaluated before the argument consumeA(std::move(a)). ``` Treating this scenario correctly has required rewriting the logic in bugprone-use-after-move that governs whether the use happens in a later loop iteration than the move. This was previously based on an unsound heuristic (does the use come lexically before the move?); we now use a more rigourous criterion based on reachability in the CFG. Fixes #57758 Fixes #59612
1 parent 2e482b2 commit e07681e

File tree

4 files changed

+148
-17
lines changed

4 files changed

+148
-17
lines changed

clang-tools-extra/clang-tidy/bugprone/UseAfterMoveCheck.cpp

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,11 @@
1111
#include "clang/AST/Expr.h"
1212
#include "clang/AST/ExprCXX.h"
1313
#include "clang/ASTMatchers/ASTMatchers.h"
14+
#include "clang/Analysis/Analyses/CFGReachabilityAnalysis.h"
1415
#include "clang/Analysis/CFG.h"
1516
#include "clang/Lex/Lexer.h"
1617
#include "llvm/ADT/STLExtras.h"
18+
#include "llvm/ADT/SmallPtrSet.h"
1719

1820
#include "../utils/ExprSequence.h"
1921
#include "../utils/Matchers.h"
@@ -34,7 +36,12 @@ struct UseAfterMove {
3436
const DeclRefExpr *DeclRef;
3537

3638
// Is the order in which the move and the use are evaluated undefined?
37-
bool EvaluationOrderUndefined;
39+
bool EvaluationOrderUndefined = false;
40+
41+
// Does the use happen in a later loop iteration than the move?
42+
//
43+
// We default to false and change it to true if required in find().
44+
bool UseHappensInLaterLoopIteration = false;
3845
};
3946

4047
/// Finds uses of a variable after a move (and maintains state required by the
@@ -48,7 +55,7 @@ class UseAfterMoveFinder {
4855
// use-after-move is found, writes information about it to 'TheUseAfterMove'.
4956
// Returns whether a use-after-move was found.
5057
bool find(Stmt *CodeBlock, const Expr *MovingCall,
51-
const ValueDecl *MovedVariable, UseAfterMove *TheUseAfterMove);
58+
const DeclRefExpr *MovedVariable, UseAfterMove *TheUseAfterMove);
5259

5360
private:
5461
bool findInternal(const CFGBlock *Block, const Expr *MovingCall,
@@ -89,7 +96,7 @@ UseAfterMoveFinder::UseAfterMoveFinder(ASTContext *TheContext)
8996
: Context(TheContext) {}
9097

9198
bool UseAfterMoveFinder::find(Stmt *CodeBlock, const Expr *MovingCall,
92-
const ValueDecl *MovedVariable,
99+
const DeclRefExpr *MovedVariable,
93100
UseAfterMove *TheUseAfterMove) {
94101
// Generate the CFG manually instead of through an AnalysisDeclContext because
95102
// it seems the latter can't be used to generate a CFG for the body of a
@@ -110,15 +117,32 @@ bool UseAfterMoveFinder::find(Stmt *CodeBlock, const Expr *MovingCall,
110117
BlockMap = std::make_unique<StmtToBlockMap>(TheCFG.get(), Context);
111118
Visited.clear();
112119

113-
const CFGBlock *Block = BlockMap->blockContainingStmt(MovingCall);
114-
if (!Block) {
120+
const CFGBlock *MoveBlock = BlockMap->blockContainingStmt(MovingCall);
121+
if (!MoveBlock) {
115122
// This can happen if MovingCall is in a constructor initializer, which is
116123
// not included in the CFG because the CFG is built only from the function
117124
// body.
118-
Block = &TheCFG->getEntry();
125+
MoveBlock = &TheCFG->getEntry();
119126
}
120127

121-
return findInternal(Block, MovingCall, MovedVariable, TheUseAfterMove);
128+
bool Found = findInternal(MoveBlock, MovingCall, MovedVariable->getDecl(),
129+
TheUseAfterMove);
130+
131+
if (Found) {
132+
if (const CFGBlock *UseBlock =
133+
BlockMap->blockContainingStmt(TheUseAfterMove->DeclRef)) {
134+
// Does the use happen in a later loop iteration than the move?
135+
// - If they are in the same CFG block, we know the use happened in a
136+
// later iteration if we visited that block a second time.
137+
// - Otherwise, we know the use happened in a later iteration if the
138+
// move is reachable from the use.
139+
CFGReverseBlockReachabilityAnalysis CFA(*TheCFG);
140+
TheUseAfterMove->UseHappensInLaterLoopIteration =
141+
UseBlock == MoveBlock ? Visited.contains(UseBlock)
142+
: CFA.isReachable(UseBlock, MoveBlock);
143+
}
144+
}
145+
return Found;
122146
}
123147

124148
bool UseAfterMoveFinder::findInternal(const CFGBlock *Block,
@@ -394,7 +418,7 @@ static void emitDiagnostic(const Expr *MovingCall, const DeclRefExpr *MoveArg,
394418
"there is no guarantee about the order in which they are evaluated",
395419
DiagnosticIDs::Note)
396420
<< IsMove;
397-
} else if (UseLoc < MoveLoc || Use.DeclRef == MoveArg) {
421+
} else if (Use.UseHappensInLaterLoopIteration) {
398422
Check->diag(UseLoc,
399423
"the use happens in a later loop iteration than the "
400424
"%select{forward|move}0",
@@ -495,7 +519,7 @@ void UseAfterMoveCheck::check(const MatchFinder::MatchResult &Result) {
495519
for (Stmt *CodeBlock : CodeBlocks) {
496520
UseAfterMoveFinder Finder(Result.Context);
497521
UseAfterMove Use;
498-
if (Finder.find(CodeBlock, MovingCall, Arg->getDecl(), &Use))
522+
if (Finder.find(CodeBlock, MovingCall, Arg, &Use))
499523
emitDiagnostic(MovingCall, Arg, Use, this, Result.Context,
500524
determineMoveType(MoveDecl));
501525
}

clang-tools-extra/clang-tidy/utils/ExprSequence.cpp

Lines changed: 62 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -55,12 +55,18 @@ bool isDescendantOrEqual(const Stmt *Descendant, const Stmt *Ancestor,
5555
ASTContext *Context) {
5656
if (Descendant == Ancestor)
5757
return true;
58-
for (const Stmt *Parent : getParentStmts(Descendant, Context)) {
59-
if (isDescendantOrEqual(Parent, Ancestor, Context))
60-
return true;
61-
}
58+
return llvm::any_of(getParentStmts(Descendant, Context),
59+
[Ancestor, Context](const Stmt *Parent) {
60+
return isDescendantOrEqual(Parent, Ancestor, Context);
61+
});
62+
}
6263

63-
return false;
64+
bool isDescendantOfArgs(const Stmt *Descendant, const CallExpr *Call,
65+
ASTContext *Context) {
66+
return llvm::any_of(Call->arguments(),
67+
[Descendant, Context](const Expr *Arg) {
68+
return isDescendantOrEqual(Descendant, Arg, Context);
69+
});
6470
}
6571

6672
llvm::SmallVector<const InitListExpr *>
@@ -95,9 +101,59 @@ bool ExprSequence::inSequence(const Stmt *Before, const Stmt *After) const {
95101
return true;
96102
}
97103

104+
SmallVector<const Stmt *, 1> BeforeParents = getParentStmts(Before, Context);
105+
106+
// Since C++17, the callee of a call expression is guaranteed to be sequenced
107+
// before all of the arguments.
108+
// We handle this as a special case rather than using the general
109+
// `getSequenceSuccessor` logic above because the callee expression doesn't
110+
// have an unambiguous successor; the order in which arguments are evaluated
111+
// is indeterminate.
112+
for (const Stmt *Parent : BeforeParents) {
113+
// Special case: If the callee is a `MemberExpr` with a `DeclRefExpr` as its
114+
// base, we consider it to be sequenced _after_ the arguments. This is
115+
// because the variable referenced in the base will only actually be
116+
// accessed when the call happens, i.e. once all of the arguments have been
117+
// evaluated. This has no basis in the C++ standard, but it reflects actual
118+
// behavior that is relevant to a use-after-move scenario:
119+
//
120+
// ```
121+
// a.bar(consumeA(std::move(a));
122+
// ```
123+
//
124+
// In this example, we end up accessing `a` after it has been moved from,
125+
// even though nominally the callee `a.bar` is evaluated before the argument
126+
// `consumeA(std::move(a))`. Note that this is not specific to C++17, so
127+
// we implement this logic unconditionally.
128+
if (const auto *Call = dyn_cast<CXXMemberCallExpr>(Parent)) {
129+
if (is_contained(Call->arguments(), Before) &&
130+
isa<DeclRefExpr>(
131+
Call->getImplicitObjectArgument()->IgnoreParenImpCasts()) &&
132+
isDescendantOrEqual(After, Call->getImplicitObjectArgument(),
133+
Context))
134+
return true;
135+
136+
// We need this additional early exit so that we don't fall through to the
137+
// more general logic below.
138+
if (const auto *Member = dyn_cast<MemberExpr>(Before);
139+
Member && Call->getCallee() == Member &&
140+
isa<DeclRefExpr>(Member->getBase()->IgnoreParenImpCasts()) &&
141+
isDescendantOfArgs(After, Call, Context))
142+
return false;
143+
}
144+
145+
if (!Context->getLangOpts().CPlusPlus17)
146+
continue;
147+
148+
if (const auto *Call = dyn_cast<CallExpr>(Parent);
149+
Call && Call->getCallee() == Before &&
150+
isDescendantOfArgs(After, Call, Context))
151+
return true;
152+
}
153+
98154
// If 'After' is a parent of 'Before' or is sequenced after one of these
99155
// parents, we know that it is sequenced after 'Before'.
100-
for (const Stmt *Parent : getParentStmts(Before, Context)) {
156+
for (const Stmt *Parent : BeforeParents) {
101157
if (Parent == After || inSequence(Parent, After))
102158
return true;
103159
}

clang-tools-extra/docs/ReleaseNotes.rst

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -254,7 +254,10 @@ Changes in existing checks
254254

255255
- Improved :doc:`bugprone-use-after-move
256256
<clang-tidy/checks/bugprone/use-after-move>` check to also handle
257-
calls to ``std::forward``.
257+
calls to ``std::forward``. Fixed sequencing of designated initializers. Fixed
258+
sequencing of callees: In C++17 and later, the callee of a function is guaranteed
259+
to be sequenced before the arguments, so don't warn if the use happens in the
260+
callee and the move happens in one of the arguments.
258261

259262
- Improved :doc:`cppcoreguidelines-missing-std-forward
260263
<clang-tidy/checks/cppcoreguidelines/missing-std-forward>` check by no longer

clang-tools-extra/test/clang-tidy/checkers/bugprone/use-after-move.cpp

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
// RUN: %check_clang_tidy -std=c++11 -check-suffixes=,CXX11 %s bugprone-use-after-move %t -- -- -fno-delayed-template-parsing
12
// RUN: %check_clang_tidy -std=c++17-or-later %s bugprone-use-after-move %t -- -- -fno-delayed-template-parsing
23

34
typedef decltype(nullptr) nullptr_t;
@@ -135,6 +136,7 @@ class A {
135136
A &operator=(A &&);
136137

137138
void foo() const;
139+
void bar(int i) const;
138140
int getInt() const;
139141

140142
operator bool() const;
@@ -576,6 +578,19 @@ void useAndMoveInLoop() {
576578
std::move(a);
577579
}
578580
}
581+
// Same as above, but the use and the move are in different CFG blocks.
582+
{
583+
A a;
584+
for (int i = 0; i < 10; ++i) {
585+
if (i < 10)
586+
a.foo();
587+
// CHECK-NOTES: [[@LINE-1]]:9: warning: 'a' used after it was moved
588+
// CHECK-NOTES: [[@LINE+3]]:9: note: move occurred here
589+
// CHECK-NOTES: [[@LINE-3]]:9: note: the use happens in a later loop
590+
if (i < 10)
591+
std::move(a);
592+
}
593+
}
579594
// However, this case shouldn't be flagged -- the scope of the declaration of
580595
// 'a' is important.
581596
{
@@ -1352,6 +1367,40 @@ void ifWhileAndSwitchSequenceInitDeclAndCondition() {
13521367
}
13531368
}
13541369

1370+
// In a function call, the expression that determines the callee is sequenced
1371+
// before the arguments -- but only in C++17 and later.
1372+
namespace CalleeSequencedBeforeArguments {
1373+
int consumeA(std::unique_ptr<A> a);
1374+
int consumeA(A &&a);
1375+
1376+
void calleeSequencedBeforeArguments() {
1377+
{
1378+
std::unique_ptr<A> a;
1379+
a->bar(consumeA(std::move(a)));
1380+
// CHECK-NOTES-CXX11: [[@LINE-1]]:5: warning: 'a' used after it was moved
1381+
// CHECK-NOTES-CXX11: [[@LINE-2]]:21: note: move occurred here
1382+
// CHECK-NOTES-CXX11: [[@LINE-3]]:5: note: the use and move are unsequenced
1383+
}
1384+
{
1385+
std::unique_ptr<A> a;
1386+
std::unique_ptr<A> getArg(std::unique_ptr<A> a);
1387+
getArg(std::move(a))->bar(a->getInt());
1388+
// CHECK-NOTES: [[@LINE-1]]:31: warning: 'a' used after it was moved
1389+
// CHECK-NOTES: [[@LINE-2]]:12: note: move occurred here
1390+
// CHECK-NOTES-CXX11: [[@LINE-3]]:31: note: the use and move are unsequenced
1391+
}
1392+
{
1393+
A a;
1394+
// Nominally, the callee `a.bar` is evaluated before the argument
1395+
// `consumeA(std::move(a))`, but in effect `a` is only accessed after the
1396+
// call to `A::bar()` happens, i.e. after the argument has been evaluted.
1397+
a.bar(consumeA(std::move(a)));
1398+
// CHECK-NOTES: [[@LINE-1]]:5: warning: 'a' used after it was moved
1399+
// CHECK-NOTES: [[@LINE-2]]:11: note: move occurred here
1400+
}
1401+
}
1402+
} // namespace CalleeSequencedBeforeArguments
1403+
13551404
// Some statements in templates (e.g. null, break and continue statements) may
13561405
// be shared between the uninstantiated and instantiated versions of the
13571406
// template and therefore have multiple parents. Make sure the sequencing code
@@ -1469,7 +1518,6 @@ class CtorInitOrder {
14691518
// CHECK-NOTES: [[@LINE-1]]:11: warning: 'val' used after it was moved
14701519
s{std::move(val)} {} // wrong order
14711520
// CHECK-NOTES: [[@LINE-1]]:9: note: move occurred here
1472-
// CHECK-NOTES: [[@LINE-4]]:11: note: the use happens in a later loop iteration than the move
14731521

14741522
private:
14751523
bool a;

0 commit comments

Comments
 (0)