Skip to content
Open
Show file tree
Hide file tree
Changes from 42 commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
3abbce9
[clang-tidy] Add check performance-lost-std-move
segoon May 12, 2025
4a1a77b
f(x, x)
segoon May 12, 2025
f74066d
test
segoon May 12, 2025
edd4800
review fixes
segoon May 26, 2025
5359c69
auto -> Expr
segoon May 26, 2025
5bb6af6
FixIt
segoon May 26, 2025
592d79b
f((x))
segoon May 26, 2025
1710c2f
w
segoon May 31, 2025
b4824ef
Merge remote-tracking branch 'origin/main' into feature/lost-std-move
segoon May 31, 2025
60e4aca
thread_local, extern, static
segoon May 31, 2025
a27b1b3
better test
segoon May 31, 2025
6aacd41
fixes
segoon Jun 1, 2025
71cd708
[=] test
segoon Jun 1, 2025
402ba55
fix
segoon Jun 1, 2025
fdf01b6
docs
segoon Jun 1, 2025
24357a2
format
segoon Jun 1, 2025
b48425b
format
segoon Jun 1, 2025
986d6aa
Merge remote-tracking branch 'origin/main' into feature/lost-std-move
segoon Jun 1, 2025
5fb15e1
w
segoon Jun 23, 2025
4a1d653
Merge remote-tracking branch 'origin/main' into feature/lost-std-move
segoon Sep 15, 2025
0d612ec
80 symbols
segoon Sep 15, 2025
6f55a1c
``
segoon Sep 15, 2025
8963056
StrictMode
segoon Sep 15, 2025
55c4d16
test
segoon Sep 15, 2025
ee844fd
option
segoon Sep 15, 2025
2fd53f6
``
segoon Sep 15, 2025
54571f9
default
segoon Sep 16, 2025
d28087b
alphabetical order
segoon Sep 16, 2025
b212770
``
segoon Sep 16, 2025
aa46750
fix UB
segoon Sep 16, 2025
4ae1247
format
segoon Sep 16, 2025
62e4951
line 80
segoon Sep 16, 2025
e12c39c
rm \n
segoon Sep 16, 2025
97db45c
rm \n
segoon Sep 16, 2025
7b091cc
add test
segoon Sep 24, 2025
9816cdb
cxxConstructExpr()
segoon Sep 24, 2025
6b31812
tidy fixes
segoon Sep 24, 2025
e8b2d48
fixes
segoon Sep 24, 2025
777eacd
text changes
segoon Sep 30, 2025
67790b8
invert logic
segoon Sep 30, 2025
708317b
Merge remote-tracking branch 'origin/main' into feature/lost-std-move
segoon Sep 30, 2025
ed6d376
fix doc
segoon Sep 30, 2025
4f62ad9
fix
segoon Oct 2, 2025
19731c0
rewording
segoon Oct 17, 2025
9218dfc
Update clang-tools-extra/docs/clang-tidy/checks/performance/lost-std-…
segoon Oct 20, 2025
6e3eb06
wrap doc line
segoon Oct 21, 2025
2a82056
auto -> type
segoon Oct 21, 2025
60bcc1a
Merge remote-tracking branch 'segoon/feature/lost-std-move' into feat…
segoon Oct 21, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions clang-tools-extra/clang-tidy/performance/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ add_clang_library(clangTidyPerformanceModule STATIC
InefficientAlgorithmCheck.cpp
InefficientStringConcatenationCheck.cpp
InefficientVectorOperationCheck.cpp
LostStdMoveCheck.cpp
MoveConstArgCheck.cpp
MoveConstructorInitCheck.cpp
NoAutomaticMoveCheck.cpp
Expand Down
196 changes: 196 additions & 0 deletions clang-tools-extra/clang-tidy/performance/LostStdMoveCheck.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
//===----------------------------------------------------------------------===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//

#include "LostStdMoveCheck.h"
#include "clang/ASTMatchers/ASTMatchFinder.h"
#include "clang/Lex/Lexer.h"

using namespace clang::ast_matchers;

namespace clang::tidy::performance {

template <typename Node>
static void extractNodesByIdTo(ArrayRef<BoundNodes> Matches, StringRef ID,
llvm::SmallPtrSet<const Node *, 16> &Nodes) {
for (const BoundNodes &Match : Matches)
Nodes.insert(Match.getNodeAs<Node>(ID));
}
Comment on lines +17 to +22
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of copying from clang-tools-extra/clang-tidy/utils/DeclRefExprUtils.cpp, could we expose extractNodesByIdTo as public function and reuse here


static llvm::SmallPtrSet<const DeclRefExpr *, 16>
allDeclRefExprsHonourLambda(const VarDecl &VarDecl, const Decl &Decl,
ASTContext &Context) {
auto Matches = match(
decl(forEachDescendant(
declRefExpr(to(varDecl(equalsNode(&VarDecl))),
unless(hasAncestor(lambdaExpr(hasAnyCapture(lambdaCapture(
capturesVar(varDecl(equalsNode(&VarDecl)))))))))
.bind("declRef"))),
Decl, Context);
llvm::SmallPtrSet<const DeclRefExpr *, 16> DeclRefs;
extractNodesByIdTo(Matches, "declRef", DeclRefs);
return DeclRefs;
}

static llvm::SmallPtrSet<const VarDecl *, 16>
allVarDeclsExprs(const VarDecl &VarDecl, const Decl &Decl,
ASTContext &Context) {
auto Matches = match(
decl(forEachDescendant(declRefExpr(
to(varDecl(equalsNode(&VarDecl))),
hasParent(decl(
varDecl(hasType(qualType(referenceType()))).bind("varDecl"))),
unless(hasAncestor(lambdaExpr(hasAnyCapture(
lambdaCapture(capturesVar(varDecl(equalsNode(&VarDecl))))))))))),
Decl, Context);
llvm::SmallPtrSet<const class VarDecl *, 16> VarDecls;
extractNodesByIdTo(Matches, "varDecl", VarDecls);
return VarDecls;
}

static const Expr *
getLastVarUsage(const llvm::SmallPtrSet<const DeclRefExpr *, 16> &Exprs) {
const Expr *LastExpr = nullptr;
for (const clang::DeclRefExpr *Expr : Exprs) {
if (!LastExpr)
LastExpr = Expr;

if (LastExpr->getBeginLoc() < Expr->getBeginLoc())
LastExpr = Expr;
}

return LastExpr;
}

namespace {

AST_MATCHER(CXXRecordDecl, hasTrivialMoveConstructor) {
return Node.hasDefinition() && Node.hasTrivialMoveConstructor();
}

AST_MATCHER_P(Expr, ignoreParens, ast_matchers::internal::Matcher<Expr>,
InnerMatcher) {
return InnerMatcher.matches(*Node.IgnoreParens(), Finder, Builder);
}

} // namespace

void LostStdMoveCheck::storeOptions(ClangTidyOptions::OptionMap &Opts) {
Options.store(Opts, "StrictMode", StrictMode);
}

LostStdMoveCheck::LostStdMoveCheck(StringRef Name, ClangTidyContext *Context)
: ClangTidyCheck(Name, Context),
StrictMode(Options.get("StrictMode", false)) {}

void LostStdMoveCheck::registerMatchers(MatchFinder *Finder) {
auto ReturnParent =
hasParent(expr(hasParent(cxxConstructExpr(hasParent(returnStmt())))));
Comment on lines +91 to +92
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we just use hasAncestor?


auto OutermostExpr = expr(unless(hasParent(expr())));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we ignoreParens?

auto LeafStatement = stmt(OutermostExpr);

Finder->addMatcher(
cxxConstructExpr(has(expr(has(ignoreParens(
declRefExpr(
// not "return x;"
unless(ReturnParent),
unless(hasType(namedDecl(hasName("::std::string_view")))),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about other non-owning containers? I think we need an option for it.
At least people with c++11/c++14 use boost::string_view

// non-trivial type
hasType(hasCanonicalType(hasDeclaration(cxxRecordDecl()))),
Comment on lines +103 to +104
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to just include RecordDecls or check constructors is not expensive?
We have

AST_MATCHER(QualType, isExpensiveToCopy) {
std::optional<bool> IsExpensive =
utils::type_traits::isExpensiveToCopy(Node, Finder->getASTContext());
return IsExpensive && *IsExpensive;
}

or
https://github.com/llvm/llvm-project/blob/main/clang-tools-extra/clang-tidy/utils/Matchers.h#L31-L34

// non-trivial X(X&&)
unless(hasType(hasCanonicalType(
hasDeclaration(cxxRecordDecl(hasTrivialMoveConstructor()))))),
// Not in a cycle
unless(hasAncestor(forStmt())), unless(hasAncestor(doStmt())),
unless(hasAncestor(whileStmt())),
// Not in a body of lambda
unless(hasAncestor(compoundStmt(hasAncestor(lambdaExpr())))),
// only non-X&
unless(hasDeclaration(
varDecl(hasType(qualType(lValueReferenceType()))))),
hasAncestor(LeafStatement.bind("leaf_statement")),
hasDeclaration(varDecl(hasAncestor(functionDecl().bind("func")))
.bind("decl")))
.bind("use")))))),
this);
}

void LostStdMoveCheck::check(const MatchFinder::MatchResult &Result) {
const auto *MatchedDecl = Result.Nodes.getNodeAs<VarDecl>("decl");
const auto *MatchedFunc = Result.Nodes.getNodeAs<FunctionDecl>("func");
const auto *MatchedUse = Result.Nodes.getNodeAs<Expr>("use");
const auto *MatchedLeafStatement =
Result.Nodes.getNodeAs<Stmt>("leaf_statement");

if (!MatchedDecl->hasLocalStorage()) {
// global or static variable
return;
}
Comment on lines +130 to +133
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can check for local storage in matchers


if (!StrictMode) {
llvm::SmallPtrSet<const VarDecl *, 16> AllVarDecls =
allVarDeclsExprs(*MatchedDecl, *MatchedFunc, *Result.Context);
if (!AllVarDecls.empty()) {
// x is referenced by local var, it may outlive the "use"
return;
}
}

llvm::SmallPtrSet<const DeclRefExpr *, 16> AllReferences =
allDeclRefExprsHonourLambda(*MatchedDecl, *MatchedFunc, *Result.Context);
const Expr *LastUsage = getLastVarUsage(AllReferences);

if (LastUsage && LastUsage->getBeginLoc() > MatchedUse->getBeginLoc()) {
// "use" is not the last reference to x
return;
}

if (LastUsage &&
LastUsage->getSourceRange() != MatchedUse->getSourceRange()) {
return;
}
Comment on lines +153 to +156
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we write such condition, does it matter to check for LastUsage->getBeginLoc() > MatchedUse->getBeginLoc() above?
Source range is begin loc + end loc so if any of the locations are mismatched - we return. This case must cover the previous case, or I'm missing something?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And why do we check this? should it be == instead to check that be don't have last usage equal to current matched variable?


// Calculate X usage count in the statement
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is X?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be number of usage?

llvm::SmallPtrSet<const DeclRefExpr *, 16> DeclRefs;

SmallVector<BoundNodes, 1> Matches = match(
findAll(
declRefExpr(to(varDecl(equalsNode(MatchedDecl))),
unless(hasAncestor(lambdaExpr(hasAnyCapture(lambdaCapture(
capturesVar(varDecl(equalsNode(MatchedDecl))))))))

)
.bind("ref")),
*MatchedLeafStatement, *Result.Context);
extractNodesByIdTo(Matches, "ref", DeclRefs);
if (DeclRefs.size() > 1) {
// Unspecified order of evaluation, e.g. f(x, x)
return;
}

const SourceManager &Source = Result.Context->getSourceManager();
const auto Range =
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

auto should not be used - type is not spelled explicitly or iterator.

CharSourceRange::getTokenRange(MatchedUse->getSourceRange());
const StringRef NeedleExprCode =
Lexer::getSourceText(Range, Source, Result.Context->getLangOpts());

if (NeedleExprCode == "=") {
diag(MatchedUse->getBeginLoc(), "could be std::move()")
<< FixItHint::CreateInsertion(MatchedUse->getBeginLoc(),
(MatchedDecl->getName() +
" = std::move(" +
MatchedDecl->getName() + "),")
.str());
} else {
diag(MatchedUse->getBeginLoc(), "could be std::move()")
<< FixItHint::CreateReplacement(
Range, ("std::move(" + NeedleExprCode + ")").str());
}
}

} // namespace clang::tidy::performance
38 changes: 38 additions & 0 deletions clang-tools-extra/clang-tidy/performance/LostStdMoveCheck.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
//===----------------------------------------------------------------------===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//

#ifndef LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_PERFORMANCE_LOSTSTDMOVECHECK_H
#define LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_PERFORMANCE_LOSTSTDMOVECHECK_H

#include "../ClangTidyCheck.h"

namespace clang::tidy::performance {

/// Warns if copy constructor is used instead of std::move() and suggests a fix.
/// It honours cycles, lambdas, and unspecified call order in compound
/// expressions.
///
/// For the user-facing documentation see:
/// https://clang.llvm.org/extra/clang-tidy/checks/performance/lost-std-move.html
class LostStdMoveCheck : public ClangTidyCheck {
public:
LostStdMoveCheck(StringRef Name, ClangTidyContext *Context);
void registerMatchers(ast_matchers::MatchFinder *Finder) override;
void check(const ast_matchers::MatchFinder::MatchResult &Result) override;
void storeOptions(ClangTidyOptions::OptionMap &Opts) override;
bool isLanguageVersionSupported(const LangOptions &LangOpts) const override {
return LangOpts.CPlusPlus11;
}

private:
const bool StrictMode;
};

} // namespace clang::tidy::performance

#endif // LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_PERFORMANCE_LOSTSTDMOVECHECK_H
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
#include "InefficientAlgorithmCheck.h"
#include "InefficientStringConcatenationCheck.h"
#include "InefficientVectorOperationCheck.h"
#include "LostStdMoveCheck.h"
#include "MoveConstArgCheck.h"
#include "MoveConstructorInitCheck.h"
#include "NoAutomaticMoveCheck.h"
Expand Down Expand Up @@ -49,6 +50,7 @@ class PerformanceModule : public ClangTidyModule {
"performance-inefficient-string-concatenation");
CheckFactories.registerCheck<InefficientVectorOperationCheck>(
"performance-inefficient-vector-operation");
CheckFactories.registerCheck<LostStdMoveCheck>("performance-lost-std-move");
CheckFactories.registerCheck<MoveConstArgCheck>(
"performance-move-const-arg");
CheckFactories.registerCheck<MoveConstructorInitCheck>(
Expand Down
5 changes: 5 additions & 0 deletions clang-tools-extra/docs/ReleaseNotes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,11 @@ New checks
Finds virtual function overrides with different visibility than the function
in the base class.

- New :doc:`performance-lost-std-move
<clang-tidy/checks/performance/lost-std-move>` check.

Warns if copy constructor is used instead of ``std::move()`` and suggests a fix.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Warns if copy constructor is used instead of ``std::move()`` and suggests a fix.
Warns if copy constructor is used instead of ``std::move()`` and suggests a
fix.


- New :doc:`readability-redundant-parentheses
<clang-tidy/checks/readability/redundant-parentheses>` check.

Expand Down
1 change: 1 addition & 0 deletions clang-tools-extra/docs/clang-tidy/checks/list.rst
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,7 @@ Clang-Tidy Checks
:doc:`performance-inefficient-algorithm <performance/inefficient-algorithm>`, "Yes"
:doc:`performance-inefficient-string-concatenation <performance/inefficient-string-concatenation>`,
:doc:`performance-inefficient-vector-operation <performance/inefficient-vector-operation>`, "Yes"
:doc:`performance-lost-std-move <performance/lost-std-move>`, "Yes"
:doc:`performance-move-const-arg <performance/move-const-arg>`, "Yes"
:doc:`performance-move-constructor-init <performance/move-constructor-init>`,
:doc:`performance-no-automatic-move <performance/no-automatic-move>`,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
.. title:: clang-tidy - performance-lost-std-move

performance-lost-std-move
=========================

Warns if copy constructor is used instead of ``std::move()`` and suggests a fix.
It honours cycles, lambdas, and unspecified call order in compound expressions.

.. code-block:: c++

void f(X);

void g(X x) {
f(x); // warning: Could be std::move() [performance-lost-std-move]
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Give examples when check will not warn


It finds the last local variable usage, and if it is a copy, emits a warning.
The check is based on pure AST matching and doesn't take into account any
data flow information. Thus, it doesn't catch assign-after-copy cases.

Also it does notice variable references "behind the scenes":

.. code-block:: c++

void f(X);

void g(X x) {
auto &y = x;
f(x); // does not emit a warning
y.f(); // because x is still used via y
}

If you want to ignore assigns to reference variables, set :option: `StrictMode`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
If you want to ignore assigns to reference variables, set :option: `StrictMode`
If you want to ignore assigns to reference variables, set :option:`StrictMode`

This is statement. See :program: in clang-tools-extra/docs/ReleaseNotes.rst as example.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks! sorry for so many small typos :(

to `true`.


Options
-------

.. option:: StrictMode

A variable ``X`` can be referenced by another variable ``R``. In this case
the last variable usage might be not from ``X``, but from ``R``. It is quite
difficult to find in a large function, so if the plugin sees some ``R``
references ``X``, then it will not emit a warning for ``X`` not to provoke
false positive. If you're sure that such references don't extend ``X``
lifetime and ready to handle possible false positives, then set `StrictMode`
to `true`. Defaults to `false`.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
to `true`. Defaults to `false`.
to `true`. Default is `false`.

Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// RUN: %check_clang_tidy %s performance-lost-std-move %t -- -config='{CheckOptions: {performance-lost-std-move.StrictMode: true}}'

namespace std {

template<typename T>
class shared_ptr {
public:
T& operator*() { return reinterpret_cast<T&>(*this); }
shared_ptr() {}
shared_ptr(const shared_ptr<T>&) {}
};

template<typename T>
T&& move(T&)
{
}

} // namespace std

int f(std::shared_ptr<int>);

void f_copy_after_ref()
{
std::shared_ptr<int> ptr;
auto& ref = ptr;
f(ptr);
// CHECK-MESSAGES: :[[@LINE-1]]:5: warning: could be std::move() [performance-lost-std-move]
// CHECK-FIXES: f(std::move(ptr));
*ref = 1;
}
Loading