-
Notifications
You must be signed in to change notification settings - Fork 13.5k
[clang-tidy] doesNotMutateObject
: Handle calls to member functions …
#94362
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[clang-tidy] doesNotMutateObject
: Handle calls to member functions …
#94362
Conversation
…and operators that have non-const overloads. This allows `unnecessary-copy-initialization` to warn on more cases. The common case is a class with a a set of const/non-sconst overloads (e.g. std::vector::operator[]). ``` void F() { std::vector<Expensive> v; // ... const Expensive e = v[i]; } ```
@llvm/pr-subscribers-clang-tools-extra @llvm/pr-subscribers-clang-tidy Author: Clement Courbet (legrosbuffle) Changes…and operators that have non-const overloads. This allows The common case is a class with a a set of const/non-sconst overloads (e.g. std::vector::operator[]).
Full diff: https://github.com/llvm/llvm-project/pull/94362.diff 5 Files Affected:
diff --git a/clang-tools-extra/clang-tidy/performance/UnnecessaryCopyInitialization.cpp b/clang-tools-extra/clang-tidy/performance/UnnecessaryCopyInitialization.cpp
index 9beb185cba929..78a1f9a73687d 100644
--- a/clang-tools-extra/clang-tidy/performance/UnnecessaryCopyInitialization.cpp
+++ b/clang-tools-extra/clang-tidy/performance/UnnecessaryCopyInitialization.cpp
@@ -75,16 +75,15 @@ void recordRemoval(const DeclStmt &Stmt, ASTContext &Context,
}
}
-AST_MATCHER_FUNCTION_P(StatementMatcher, isConstRefReturningMethodCall,
+AST_MATCHER_FUNCTION_P(StatementMatcher, isRefReturningMethodCall,
std::vector<StringRef>, ExcludedContainerTypes) {
// Match method call expressions where the `this` argument is only used as
- // const, this will be checked in `check()` part. This returned const
- // reference is highly likely to outlive the local const reference of the
- // variable being declared. The assumption is that the const reference being
- // returned either points to a global static variable or to a member of the
- // called object.
+ // const, this will be checked in `check()` part. This returned reference is
+ // highly likely to outlive the local const reference of the variable being
+ // declared. The assumption is that the reference being returned either points
+ // to a global static variable or to a member of the called object.
const auto MethodDecl =
- cxxMethodDecl(returns(hasCanonicalType(matchers::isReferenceToConst())))
+ cxxMethodDecl(returns(hasCanonicalType(referenceType())))
.bind(MethodDeclId);
const auto ReceiverExpr =
ignoringParenImpCasts(declRefExpr(to(varDecl().bind(ObjectArgId))));
@@ -121,7 +120,7 @@ AST_MATCHER_FUNCTION_P(StatementMatcher, initializerReturnsReferenceToConst,
declRefExpr(to(varDecl(hasLocalStorage()).bind(OldVarDeclId)));
return expr(
anyOf(isConstRefReturningFunctionCall(),
- isConstRefReturningMethodCall(ExcludedContainerTypes),
+ isRefReturningMethodCall(ExcludedContainerTypes),
ignoringImpCasts(OldVarDeclRef),
ignoringImpCasts(unaryOperator(hasOperatorName("&"),
hasUnaryOperand(OldVarDeclRef)))));
@@ -259,9 +258,9 @@ void UnnecessaryCopyInitialization::registerMatchers(MatchFinder *Finder) {
.bind("blockStmt");
};
- Finder->addMatcher(LocalVarCopiedFrom(anyOf(isConstRefReturningFunctionCall(),
- isConstRefReturningMethodCall(
- ExcludedContainerTypes))),
+ Finder->addMatcher(LocalVarCopiedFrom(anyOf(
+ isConstRefReturningFunctionCall(),
+ isRefReturningMethodCall(ExcludedContainerTypes))),
this);
Finder->addMatcher(LocalVarCopiedFrom(declRefExpr(
diff --git a/clang-tools-extra/clang-tidy/utils/DeclRefExprUtils.cpp b/clang-tools-extra/clang-tidy/utils/DeclRefExprUtils.cpp
index a48e45e135681..4ee8a3628061b 100644
--- a/clang-tools-extra/clang-tidy/utils/DeclRefExprUtils.cpp
+++ b/clang-tools-extra/clang-tidy/utils/DeclRefExprUtils.cpp
@@ -36,6 +36,111 @@ void extractNodesByIdTo(ArrayRef<BoundNodes> Matches, StringRef ID,
Nodes.insert(Match.getNodeAs<Node>(ID));
}
+// If `D` has a const-qualified overload with otherwise identical
+// ref-qualifiers, returns that overload.
+const CXXMethodDecl *findConstOverload(const CXXMethodDecl &D) {
+ assert(!D.isConst());
+
+ DeclContext::lookup_result lookup_result =
+ D.getParent()->lookup(D.getNameInfo().getName());
+ if (lookup_result.isSingleResult()) {
+ // No overload.
+ return nullptr;
+ }
+ for (const Decl *overload : lookup_result) {
+ const CXXMethodDecl *candidate = dyn_cast<CXXMethodDecl>(overload);
+ if (candidate && !candidate->isDeleted() && candidate->isConst() &&
+ candidate->getRefQualifier() == D.getRefQualifier()) {
+ return candidate;
+ }
+ }
+ return nullptr;
+}
+
+// Returns true if both types refer to the same to the same type,
+// ignoring the const-qualifier.
+bool isSameTypeIgnoringConst(QualType A, QualType B) {
+ A = A.getCanonicalType();
+ B = B.getCanonicalType();
+ A.addConst();
+ B.addConst();
+ return A == B;
+}
+
+// Returns true if both types are pointers or reference to the same type,
+// ignoring the const-qualifier.
+bool pointsToSameTypeIgnoringConst(QualType A, QualType B) {
+ assert(A->isPointerType() || A->isReferenceType());
+ assert(B->isPointerType() || B->isReferenceType());
+ return isSameTypeIgnoringConst(A->getPointeeType(), B->getPointeeType());
+}
+
+// Return true if non-const member function `M` likely does not mutate `*this`.
+//
+// Note that if the member call selects a method/operator `f` that
+// is not const-qualified, then we also consider that the object is
+// not mutated if:
+// - (A) there is a const-qualified overload `cf` of `f` that has
+// the
+// same ref-qualifiers;
+// - (B) * `f` returns a value, or
+// * if `f` returns a `T&`, `cf` returns a `const T&` (up to
+// possible aliases such as `reference` and
+// `const_reference`), or
+// * if `f` returns a `T*`, `cf` returns a `const T*` (up to
+// possible aliases).
+// - (C) the result of the call is not mutated.
+//
+// The assumption that `cf` has the same semantics as `f`.
+// For example:
+// - In `std::vector<T> v; const T t = v[...];`, we consider that
+// expression `v[...]` does not mutate `v` as
+// `T& std::vector<T>::operator[]` has a const overload
+// `const T& std::vector<T>::operator[] const`, and the
+// result expression of type `T&` is only used as a `const T&`;
+// - In `std::map<K, V> m; V v = m.at(...);`, we consider
+// `m.at(...)` to be an immutable access for the same reason.
+// However:
+// - In `std::map<K, V> m; const V v = m[...];`, We consider that
+// `m[...]` mutates `m` as `V& std::map<K, V>::operator[]` does
+// not have a const overload.
+// - In `std::vector<T> v; T& t = v[...];`, we consider that
+// expression `v[...]` mutates `v` as the result is kept as a
+// mutable reference.
+//
+// This function checks (A) ad (B), but the caller should make sure that the
+// object is not mutated through the return value.
+bool isLikelyShallowConst(const CXXMethodDecl &M) {
+ assert(!M.isConst());
+ // The method can mutate our variable.
+
+ // (A)
+ const CXXMethodDecl *ConstOverload = findConstOverload(M);
+ if (ConstOverload == nullptr) {
+ return false;
+ }
+
+ // (B)
+ const QualType CallTy = M.getReturnType().getCanonicalType();
+ const QualType OverloadTy = ConstOverload->getReturnType().getCanonicalType();
+ if (CallTy->isReferenceType()) {
+ if (!(OverloadTy->isReferenceType() &&
+ pointsToSameTypeIgnoringConst(CallTy, OverloadTy))) {
+ return false;
+ }
+ } else if (CallTy->isPointerType()) {
+ if (!(OverloadTy->isPointerType() &&
+ pointsToSameTypeIgnoringConst(CallTy, OverloadTy))) {
+ return false;
+ }
+ } else {
+ if (!isSameTypeIgnoringConst(CallTy, OverloadTy)) {
+ return false;
+ }
+ }
+ return true;
+}
+
// A matcher that matches DeclRefExprs that are used in ways such that the
// underlying declaration is not modified.
// If the declaration is of pointer type, `Indirections` specifies the level
@@ -54,16 +159,15 @@ void extractNodesByIdTo(ArrayRef<BoundNodes> Matches, StringRef ID,
// matches (A).
//
AST_MATCHER_P(DeclRefExpr, doesNotMutateObject, int, Indirections) {
- // We walk up the parents of the DeclRefExpr recursively until we end up on a
- // parent that cannot modify the underlying object. There are a few kinds of
- // expressions:
- // - Those that cannot be used to mutate the underlying object. We can stop
+ // We walk up the parents of the DeclRefExpr recursively. There are a few
+ // kinds of expressions:
+ // - Those that cannot be used to mutate the underlying variable. We can stop
// recursion there.
- // - Those that can be used to mutate the underlying object in analyzable
+ // - Those that can be used to mutate the underlying variable in analyzable
// ways (such as taking the address or accessing a subobject). We have to
// examine the parents.
// - Those that we don't know how to analyze. In that case we stop there and
- // we assume that they can mutate the underlying expression.
+ // we assume that they can modify the expression.
struct StackEntry {
StackEntry(const Expr *E, int Indirections)
@@ -90,7 +194,7 @@ AST_MATCHER_P(DeclRefExpr, doesNotMutateObject, int, Indirections) {
assert(Ty->isPointerType());
Ty = Ty->getPointeeType().getCanonicalType();
}
- if (Ty.isConstQualified())
+ if (Ty->isVoidType() || Ty.isConstQualified())
continue;
// Otherwise we have to look at the parents to see how the expression is
@@ -159,11 +263,56 @@ AST_MATCHER_P(DeclRefExpr, doesNotMutateObject, int, Indirections) {
// The method call cannot mutate our variable.
continue;
}
+ if (isLikelyShallowConst(*Method)) {
+ // We still have to check that the object is not modified through
+ // the method's return value (C).
+ const auto MemberParents = Ctx.getParents(*Member);
+ assert(MemberParents.size() == 1);
+ const CallExpr *Call = MemberParents[0].get<CallExpr>();
+ // If `o` is an object of class type and `f` is a member function,
+ // then `o.f` has to be used as part of a call expression.
+ assert(Call != nullptr && "member function has to be called");
+ Stack.emplace_back(
+ Call,
+ Method->getReturnType().getCanonicalType()->isPointerType()
+ ? 1
+ : 0);
+ continue;
+ }
return false;
}
Stack.emplace_back(Member, 0);
continue;
}
+ if (const auto *const OpCall = dyn_cast<CXXOperatorCallExpr>(P)) {
+ // Operator calls have function call syntax. The `*this` parameter
+ // is the first parameter.
+ if (OpCall->getNumArgs() == 0 || OpCall->getArg(0) != Entry.E) {
+ return false;
+ }
+ const auto *const Method =
+ dyn_cast<CXXMethodDecl>(OpCall->getDirectCallee());
+
+ if (Method == nullptr) {
+ // This is not a member operator. Typically, a friend operator. These
+ // are handled like function calls.
+ return false;
+ }
+
+ if (Method->isConst() || Method->isStatic()) {
+ continue;
+ }
+ if (isLikelyShallowConst(*Method)) {
+ // We still have to check that the object is not modified through
+ // the operator's return value (C).
+ Stack.emplace_back(
+ OpCall,
+ Method->getReturnType().getCanonicalType()->isPointerType() ? 1
+ : 0);
+ continue;
+ }
+ return false;
+ }
if (const auto *const Op = dyn_cast<UnaryOperator>(P)) {
switch (Op->getOpcode()) {
diff --git a/clang-tools-extra/docs/ReleaseNotes.rst b/clang-tools-extra/docs/ReleaseNotes.rst
index 33b65caf2b02c..b8392d5b2d58e 100644
--- a/clang-tools-extra/docs/ReleaseNotes.rst
+++ b/clang-tools-extra/docs/ReleaseNotes.rst
@@ -362,8 +362,10 @@ Changes in existing checks
- Improved :doc:`performance-unnecessary-copy-initialization
<clang-tidy/checks/performance/unnecessary-copy-initialization>` check by
detecting more cases of constant access. In particular, pointers can be
- analyzed, se the check now handles the common patterns
+ analyzed, so the check now handles the common patterns
`const auto e = (*vector_ptr)[i]` and `const auto e = vector_ptr->at(i);`.
+ Calls to mutable function where there exists a `const` overload are also
+ handled.
- Improved :doc:`readability-avoid-return-with-void-value
<clang-tidy/checks/readability/avoid-return-with-void-value>` check by adding
diff --git a/clang-tools-extra/test/clang-tidy/checkers/performance/unnecessary-copy-initialization.cpp b/clang-tools-extra/test/clang-tidy/checkers/performance/unnecessary-copy-initialization.cpp
index 92625cc1332e2..f259552dc8f1d 100644
--- a/clang-tools-extra/test/clang-tidy/checkers/performance/unnecessary-copy-initialization.cpp
+++ b/clang-tools-extra/test/clang-tidy/checkers/performance/unnecessary-copy-initialization.cpp
@@ -32,6 +32,9 @@ struct ExpensiveToCopyType {
template <typename T>
struct Container {
+ using reference = T&;
+ using const_reference = const T&;
+
bool empty() const;
const T& operator[](int) const;
const T& operator[](int);
@@ -42,8 +45,8 @@ struct Container {
void nonConstMethod();
bool constMethod() const;
- const T& at(int) const;
- T& at(int);
+ reference at(int) const;
+ const_reference at(int);
};
@@ -207,6 +210,28 @@ void PositiveOperatorCallConstValueParam(const Container<ExpensiveToCopyType> C)
VarCopyConstructed.constMethod();
}
+void PositiveOperatorValueParam(Container<ExpensiveToCopyType> C) {
+ const auto AutoAssigned = C[42];
+ // CHECK-MESSAGES: [[@LINE-1]]:14: warning: the const qualified variable 'AutoAssigned'
+ // CHECK-FIXES: const auto& AutoAssigned = C[42];
+ AutoAssigned.constMethod();
+
+ const auto AutoCopyConstructed(C[42]);
+ // CHECK-MESSAGES: [[@LINE-1]]:14: warning: the const qualified variable 'AutoCopyConstructed'
+ // CHECK-FIXES: const auto& AutoCopyConstructed(C[42]);
+ AutoCopyConstructed.constMethod();
+
+ const ExpensiveToCopyType VarAssigned = C.at(42);
+ // CHECK-MESSAGES: [[@LINE-1]]:29: warning: the const qualified variable 'VarAssigned'
+ // CHECK-FIXES: const ExpensiveToCopyType& VarAssigned = C.at(42);
+ VarAssigned.constMethod();
+
+ const ExpensiveToCopyType VarCopyConstructed(C.at(42));
+ // CHECK-MESSAGES: [[@LINE-1]]:29: warning: the const qualified variable 'VarCopyConstructed'
+ // CHECK-FIXES: const ExpensiveToCopyType& VarCopyConstructed(C.at(42));
+ VarCopyConstructed.constMethod();
+}
+
void PositiveOperatorCallConstValueParamAlias(const ExpensiveToCopyContainerAlias C) {
const auto AutoAssigned = C[42];
// CHECK-MESSAGES: [[@LINE-1]]:14: warning: the const qualified variable 'AutoAssigned'
diff --git a/clang-tools-extra/unittests/clang-tidy/DeclRefExprUtilsTest.cpp b/clang-tools-extra/unittests/clang-tidy/DeclRefExprUtilsTest.cpp
index 3d9f51e2e17b0..af314d125d414 100644
--- a/clang-tools-extra/unittests/clang-tidy/DeclRefExprUtilsTest.cpp
+++ b/clang-tools-extra/unittests/clang-tidy/DeclRefExprUtilsTest.cpp
@@ -59,6 +59,9 @@ template <int Indirections> void RunTest(StringRef Snippet) {
void operator[](int);
void operator[](int) const;
+ int& at(int);
+ const int& at(int) const;
+
bool operator==(const S&) const;
int int_member;
@@ -161,9 +164,11 @@ TEST(ConstReferenceDeclRefExprsTest, ConstRefVar) {
useIntConstRef(/*const*/target.int_member);
useIntPtr(/*const*/target.ptr_member);
useIntConstPtr(&/*const*/target.int_member);
+ (void)/*const*/target.at(3);
const S& const_target_ref = /*const*/target;
const S* const_target_ptr = &/*const*/target;
+ (void)/*const*/target.at(3);
}
)");
}
@@ -187,9 +192,9 @@ TEST(ConstReferenceDeclRefExprsTest, ValueVar) {
/*const*/target.staticMethod();
target.nonConstMethod();
/*const*/target(ConstTag{});
- target[42];
+ /*const*/target[42];
/*const*/target(ConstTag{});
- target(NonConstTag{});
+ /*const*/target(NonConstTag{});
useRef(target);
usePtr(&target);
useConstRef((/*const*/target));
@@ -211,6 +216,12 @@ TEST(ConstReferenceDeclRefExprsTest, ValueVar) {
const S& const_target_ref = /*const*/target;
const S* const_target_ptr = &/*const*/target;
S* target_ptr = ⌖
+
+ (void)/*const*/target.at(3);
+ ++target.at(3);
+ const int civ = /*const*/target.at(3);
+ const int& cir = /*const*/target.at(3);
+ int& ir = target.at(3);
}
)");
}
@@ -227,7 +238,7 @@ TEST(ConstReferenceDeclRefExprsTest, RefVar) {
/*const*/target.staticMethod();
target.nonConstMethod();
/*const*/target(ConstTag{});
- target[42];
+ /*const*/target[42];
useConstRef((/*const*/target));
(/*const*/target).constMethod();
(void)(/*const*/target == /*const*/target);
@@ -249,6 +260,12 @@ TEST(ConstReferenceDeclRefExprsTest, RefVar) {
const S& const_target_ref = /*const*/target;
const S* const_target_ptr = &/*const*/target;
S* target_ptr = ⌖
+
+ (void)/*const*/target.at(3);
+ ++target.at(3);
+ const int civ = /*const*/target.at(3);
+ const int& cir = /*const*/target.at(3);
+ int& ir = target.at(3);
}
)");
}
@@ -266,8 +283,8 @@ TEST(ConstReferenceDeclRefExprsTest, PtrVar) {
/*const*/target->staticMethod();
target->nonConstMethod();
(*/*const*/target)(ConstTag{});
- (*target)[42];
- target->operator[](42);
+ (*/*const*/target)[42];
+ /*const*/target->operator[](42);
useConstRef((*/*const*/target));
(/*const*/target)->constMethod();
(void)(*/*const*/target == */*const*/target);
@@ -284,7 +301,13 @@ TEST(ConstReferenceDeclRefExprsTest, PtrVar) {
const S& const_target_ref = */*const*/target;
const S* const_target_ptr = /*const*/target;
- S* target_ptr = target; // FIXME: we could chect const usage of `target_ptr`.
+ S* target_ptr = target; // FIXME: we could chect const usage of `target_ptr`
+
+ (void)/*const*/target->at(3);
+ ++target->at(3);
+ const int civ = /*const*/target->at(3);
+ const int& cir = /*const*/target->at(3);
+ int& ir = target->at(3);
}
)");
}
@@ -319,6 +342,10 @@ TEST(ConstReferenceDeclRefExprsTest, ConstPtrVar) {
const S& const_target_ref = */*const*/target;
const S* const_target_ptr = /*const*/target;
+
+ (void)/*const*/target->at(3);
+ const int civ = /*const*/target->at(3);
+ const int& cir = /*const*/target->at(3);
}
)");
}
|
Could this also be applied for #69577? (please also mind the tickets referenced in it) |
Weirdly, those two checks are using distinct methods to infer whether the underlying container is mutated. So unfortunately this change won't improve I can have a look at unifying both in a subsequent PR. |
Thanks for looking into this.
Simply adding comments to the tickets in question, so the information is not lost to time, would suffice for now. |
clang-tools-extra/clang-tidy/performance/UnnecessaryCopyInitialization.cpp
Outdated
Show resolved
Hide resolved
for (const Decl *overload : lookup_result) { | ||
const CXXMethodDecl *candidate = dyn_cast<CXXMethodDecl>(overload); | ||
if (candidate && !candidate->isDeleted() && candidate->isConst() && | ||
candidate->getRefQualifier() == D.getRefQualifier()) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It looks like we look up by name, do we also need to confirm that the return types are the same, or would overloading otherwise not be allowed and that's why it's implied?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Overloading would be allowed in this case, but I think having a const
overload with the same parameter types is enough to say that the use is immutable (not that the case when the return value is non-const and the object might be modified through the return value is caught by (C)
). I've added a test to make this explicit (weird_overload()
).
That being said, your comment made me realize that we were not checking that the parameter types were the same. Done and added tests (at(Tag1)
). Thanks :)
return nullptr; | ||
} | ||
for (const Decl *overload : lookup_result) { | ||
const CXXMethodDecl *candidate = dyn_cast<CXXMethodDecl>(overload); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
const CXXMethodDecl *candidate = dyn_cast<CXXMethodDecl>(overload); | |
const auto *candidate = dyn_cast<CXXMethodDecl>(overload); |
Type is explicitly stated in same statement.
Originally these checks were using the same logic to determine whether the variable was mutated. I don't know why why this was changed for one of these checks. |
…and operators that have non-const overloads.
This allows
unnecessary-copy-initialization
to warn on more cases.The common case is a class with a a set of const/non-sconst overloads (e.g. std::vector::operator[]).