Skip to content
Open
Show file tree
Hide file tree
Changes from 31 commits
Commits
Show all changes
43 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
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
215 changes: 215 additions & 0 deletions clang-tools-extra/clang-tidy/performance/LostStdMoveCheck.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
//===--- LostStdMoveCheck.cpp - clang-tidy --------------------------------===//
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
//===--- LostStdMoveCheck.cpp - clang-tidy --------------------------------===//
//===----------------------------------------------------------------------===//```

//
// 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>
void extractNodesByIdTo(ArrayRef<BoundNodes> Matches, StringRef ID,
llvm::SmallPtrSet<const Node *, 16> &Nodes) {
for (const BoundNodes &Match : Matches)
Nodes.insert(Match.getNodeAs<Node>(ID));
}

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;
}

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

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

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

void LostStdMoveCheck::registerMatchers(MatchFinder *Finder) {
auto ReturnParent =
hasParent(expr(hasParent(cxxConstructExpr(hasParent(returnStmt())))));

auto OutermostExpr = expr(unless(hasParent(expr())));
auto LeafStatement = stmt(OutermostExpr);

Finder->addMatcher(
declRefExpr(
// not "return x;"
unless(ReturnParent),

Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think that such newlines improve code readability. Same in other places.

unless(hasType(namedDecl(hasName("::std::string_view")))),

// non-trivial type
hasType(hasCanonicalType(hasDeclaration(cxxRecordDecl()))),

// 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")),

anyOf(

// f(x)
hasParent(expr(hasParent(cxxConstructExpr())).bind("use_parent")),

// f((x))
hasParent(parenExpr(hasParent(
expr(hasParent(cxxConstructExpr())).bind("use_parent"))))

)

)
.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 *MatchedUseCall = Result.Nodes.getNodeAs<CallExpr>("use_parent");
const auto *MatchedLeafStatement =
Result.Nodes.getNodeAs<Stmt>("leaf_statement");

if (!MatchedDecl->hasLocalStorage())
return;

if (MatchedUseCall) {
return;
}

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;
}

// Calculate X usage count in the statement
llvm::SmallPtrSet<const DeclRefExpr *, 16> DeclRefs;

SmallVector<BoundNodes, 1> Matches = match(
findAll(declRefExpr(

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

Same below.

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 =
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 @@
//===--- LostStdMoveCheck.h - clang-tidy ------------------------*- C++ -*-===//
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
//===--- LostStdMoveCheck.h - clang-tidy ------------------------*- C++ -*-===//
//===----------------------------------------------------------------------===//

//
// 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:
/// http://clang.llvm.org/extra/clang-tidy/checks/performance/lost-std-move.html
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
/// http://clang.llvm.org/extra/clang-tidy/checks/performance/lost-std-move.html
/// 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 @@ -194,6 +194,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.

New check aliases
^^^^^^^^^^^^^^^^^

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 @@ -349,6 +349,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,47 @@
.. 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 is still used
}

If you want to ignore assigns to reference variables, set ``StrictMode``
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
If you want to ignore assigns to reference variables, set ``StrictMode``
to ``false``.
If you want to ignore assigns to reference variables, set :option:`StrictMode`
to `false`.



Options
-------

.. option:: StrictMode

A variable ``X`` can be referenced by another variable ``R``. In this case the last
Copy link
Contributor

Choose a reason for hiding this comment

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

Please follow 80 characters limit.

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 `false`. Defaults to `true`.
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: false}}'

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