https://github.com/hjanuschka created 
https://github.com/llvm/llvm-project/pull/182024

Finds function parameters declared as `const T&` that are unconditionally 
copied into local variables, and suggests passing by value with `std::move`.

When a `const T&` parameter is always copied, passing by value instead allows 
callers with rvalues to avoid the copy entirely through move construction.

```cpp
// Before
void process(const std::string &s) {
  std::string local = s;
  // use local...
}

// After
void process(std::string s) {
  std::string local = std::move(s);
  // use local...
}
```

The check only triggers when:
- The parameter type is a non-trivially-copyable class or struct
- The type has a non-deleted move constructor
- The parameter is copied via copy construction into a local variable

Constructor member initializer lists are not handled, as that case is already 
covered by `modernize-pass-by-value`.

Fixes #94798.

>From 5f950a054c76929121e4d99f06140dcef5db5d29 Mon Sep 17 00:00:00 2001
From: Helmut Januschka <[email protected]>
Date: Wed, 18 Feb 2026 14:51:56 +0100
Subject: [PATCH] [clang-tidy] Add performance-const-ref-copy check

Finds function parameters declared as const T& that are unconditionally
copied into local variables, and suggests passing by value with std::move.

Fixes #94798.
---
 .../clang-tidy/performance/CMakeLists.txt     |   1 +
 .../performance/ConstRefCopyCheck.cpp         | 104 ++++++++++++++++++
 .../performance/ConstRefCopyCheck.h           |  35 ++++++
 .../performance/PerformanceTidyModule.cpp     |   3 +
 clang-tools-extra/docs/ReleaseNotes.rst       |   6 +
 .../docs/clang-tidy/checks/list.rst           |   1 +
 .../checks/performance/const-ref-copy.rst     |  39 +++++++
 .../checkers/performance/const-ref-copy.cpp   |  84 ++++++++++++++
 8 files changed, 273 insertions(+)
 create mode 100644 
clang-tools-extra/clang-tidy/performance/ConstRefCopyCheck.cpp
 create mode 100644 clang-tools-extra/clang-tidy/performance/ConstRefCopyCheck.h
 create mode 100644 
clang-tools-extra/docs/clang-tidy/checks/performance/const-ref-copy.rst
 create mode 100644 
clang-tools-extra/test/clang-tidy/checkers/performance/const-ref-copy.cpp

diff --git a/clang-tools-extra/clang-tidy/performance/CMakeLists.txt 
b/clang-tools-extra/clang-tidy/performance/CMakeLists.txt
index 4dba117e1ee54..ee40862509325 100644
--- a/clang-tools-extra/clang-tidy/performance/CMakeLists.txt
+++ b/clang-tools-extra/clang-tidy/performance/CMakeLists.txt
@@ -5,6 +5,7 @@ set(LLVM_LINK_COMPONENTS
 
 add_clang_library(clangTidyPerformanceModule STATIC
   AvoidEndlCheck.cpp
+  ConstRefCopyCheck.cpp
   EnumSizeCheck.cpp
   FasterStringFindCheck.cpp
   ForRangeCopyCheck.cpp
diff --git a/clang-tools-extra/clang-tidy/performance/ConstRefCopyCheck.cpp 
b/clang-tools-extra/clang-tidy/performance/ConstRefCopyCheck.cpp
new file mode 100644
index 0000000000000..403c34cf98142
--- /dev/null
+++ b/clang-tools-extra/clang-tidy/performance/ConstRefCopyCheck.cpp
@@ -0,0 +1,104 @@
+//===----------------------------------------------------------------------===//
+//
+// 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 "ConstRefCopyCheck.h"
+#include "clang/AST/ASTContext.h"
+#include "clang/ASTMatchers/ASTMatchFinder.h"
+
+using namespace clang::ast_matchers;
+
+namespace clang::tidy::performance {
+
+void ConstRefCopyCheck::registerMatchers(MatchFinder *Finder) {
+  // Match a function/method/constructor with a const T& parameter where
+  // T is a non-trivially-copyable record type, and the parameter is used
+  // to copy-construct a local variable or initialize a member field.
+
+  const auto ConstRefParam =
+      parmVarDecl(hasType(qualType(references(qualType(isConstQualified())))),
+                  unless(isExpansionInSystemHeader()));
+
+  // Case 1: Local variable copy-initialized from the parameter.
+  //   void f(const T& t) { T local = t; ... }
+  Finder->addMatcher(
+      varDecl(hasLocalStorage(),
+              hasInitializer(ignoringImplicit(
+                  cxxConstructExpr(
+                      hasDeclaration(cxxConstructorDecl(isCopyConstructor())),
+                      hasArgument(0, ignoringImplicit(declRefExpr(
+                                         to(ConstRefParam.bind("param"))))))
+                      .bind("construct"))),
+              unless(isExpansionInSystemHeader()))
+          .bind("localVar"),
+      this);
+
+  // Case 2: Constructor member initializer copying the parameter.
+  //   S(const T& t) : member(t) {}
+  Finder->addMatcher(
+      cxxConstructorDecl(
+          forEachConstructorInitializer(
+              cxxCtorInitializer(
+                  isMemberInitializer(),
+                  withInitializer(ignoringImplicit(
+                      cxxConstructExpr(
+                          hasDeclaration(
+                              cxxConstructorDecl(isCopyConstructor())),
+                          hasArgument(0, ignoringImplicit(declRefExpr(
+                                             
to(ConstRefParam.bind("param"))))))
+                          .bind("construct"))))
+                  .bind("memberInit")))
+          .bind("ctor"),
+      this);
+}
+
+void ConstRefCopyCheck::check(const MatchFinder::MatchResult &Result) {
+  const auto *Param = Result.Nodes.getNodeAs<ParmVarDecl>("param");
+  if (!Param)
+    return;
+
+  // Get the underlying type (strip the reference and const).
+  const QualType ParamType = Param->getType().getNonReferenceType();
+  const QualType UnqualType = ParamType.getUnqualifiedType();
+
+  // Only warn for non-trivially-copyable types where move is beneficial.
+  if (UnqualType->isBuiltinType())
+    return;
+  const CXXRecordDecl *Record = UnqualType->getAsCXXRecordDecl();
+  if (!Record || !Record->hasDefinition())
+    return;
+  if (Record->isTriviallyCopyable())
+    return;
+  // Must have a non-deleted move constructor for the suggestion to be
+  // beneficial.
+  bool HasUsableMove = false;
+  if (Record->needsImplicitMoveConstructor()) {
+    HasUsableMove = true;
+  } else {
+    for (const CXXConstructorDecl *Ctor : Record->ctors())
+      if (Ctor->isMoveConstructor() && !Ctor->isDeleted()) {
+        HasUsableMove = true;
+        break;
+      }
+  }
+  if (!HasUsableMove)
+    return;
+
+  if (const auto *LocalVar = Result.Nodes.getNodeAs<VarDecl>("localVar")) {
+    diag(Param->getLocation(),
+         "parameter %0 is copied into local variable %1; "
+         "consider passing by value and using 'std::move'")
+        << Param << LocalVar;
+    diag(LocalVar->getLocation(), "copy is here", DiagnosticIDs::Note);
+  } else if (Result.Nodes.getNodeAs<CXXCtorInitializer>("memberInit")) {
+    // modernize-pass-by-value already handles constructor cases.
+    // Skip to avoid overlapping diagnostics.
+    return;
+  }
+}
+
+} // namespace clang::tidy::performance
diff --git a/clang-tools-extra/clang-tidy/performance/ConstRefCopyCheck.h 
b/clang-tools-extra/clang-tidy/performance/ConstRefCopyCheck.h
new file mode 100644
index 0000000000000..93f9a27b68c24
--- /dev/null
+++ b/clang-tools-extra/clang-tidy/performance/ConstRefCopyCheck.h
@@ -0,0 +1,35 @@
+//===----------------------------------------------------------------------===//
+//
+// 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_CONSTREFCOPYCHECK_H
+#define LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_PERFORMANCE_CONSTREFCOPYCHECK_H
+
+#include "../ClangTidyCheck.h"
+
+namespace clang::tidy::performance {
+
+/// Finds function parameters declared as 'const T&' that are unconditionally
+/// copied into local variables or member fields, and suggests changing the
+/// parameter to pass-by-value with 'std::move'.
+///
+/// For the user-facing documentation see:
+/// 
https://clang.llvm.org/extra/clang-tidy/checks/performance/const-ref-copy.html
+class ConstRefCopyCheck : public ClangTidyCheck {
+public:
+  ConstRefCopyCheck(StringRef Name, ClangTidyContext *Context)
+      : ClangTidyCheck(Name, Context) {}
+  void registerMatchers(ast_matchers::MatchFinder *Finder) override;
+  void check(const ast_matchers::MatchFinder::MatchResult &Result) override;
+  bool isLanguageVersionSupported(const LangOptions &LangOpts) const override {
+    return LangOpts.CPlusPlus11;
+  }
+};
+
+} // namespace clang::tidy::performance
+
+#endif // LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_PERFORMANCE_CONSTREFCOPYCHECK_H
diff --git a/clang-tools-extra/clang-tidy/performance/PerformanceTidyModule.cpp 
b/clang-tools-extra/clang-tidy/performance/PerformanceTidyModule.cpp
index 294a209e4c602..04830a5fd9b8b 100644
--- a/clang-tools-extra/clang-tidy/performance/PerformanceTidyModule.cpp
+++ b/clang-tools-extra/clang-tidy/performance/PerformanceTidyModule.cpp
@@ -9,6 +9,7 @@
 #include "../ClangTidy.h"
 #include "../ClangTidyModule.h"
 #include "AvoidEndlCheck.h"
+#include "ConstRefCopyCheck.h"
 #include "EnumSizeCheck.h"
 #include "FasterStringFindCheck.h"
 #include "ForRangeCopyCheck.h"
@@ -37,6 +38,8 @@ class PerformanceModule : public ClangTidyModule {
 public:
   void addCheckFactories(ClangTidyCheckFactories &CheckFactories) override {
     CheckFactories.registerCheck<AvoidEndlCheck>("performance-avoid-endl");
+    CheckFactories.registerCheck<ConstRefCopyCheck>(
+        "performance-const-ref-copy");
     CheckFactories.registerCheck<EnumSizeCheck>("performance-enum-size");
     CheckFactories.registerCheck<FasterStringFindCheck>(
         "performance-faster-string-find");
diff --git a/clang-tools-extra/docs/ReleaseNotes.rst 
b/clang-tools-extra/docs/ReleaseNotes.rst
index 68bab88146241..53e3f29eb8940 100644
--- a/clang-tools-extra/docs/ReleaseNotes.rst
+++ b/clang-tools-extra/docs/ReleaseNotes.rst
@@ -133,6 +133,12 @@ New checks
   Finds places where structured bindings could be used to decompose pairs and
   suggests replacing them.
 
+- New :doc:`performance-const-ref-copy
+  <clang-tidy/checks/performance/const-ref-copy>` check.
+
+  Finds ``const T&`` parameters that are unconditionally copied into
+  local variables and suggests passing by value with ``std::move``.
+
 - New :doc:`performance-string-view-conversions
   <clang-tidy/checks/performance/string-view-conversions>` check.
 
diff --git a/clang-tools-extra/docs/clang-tidy/checks/list.rst 
b/clang-tools-extra/docs/clang-tidy/checks/list.rst
index c475870ed7b31..fdfa8efd69e68 100644
--- a/clang-tools-extra/docs/clang-tidy/checks/list.rst
+++ b/clang-tools-extra/docs/clang-tidy/checks/list.rst
@@ -350,6 +350,7 @@ Clang-Tidy Checks
    :doc:`openmp-exception-escape <openmp/exception-escape>`,
    :doc:`openmp-use-default-none <openmp/use-default-none>`,
    :doc:`performance-avoid-endl <performance/avoid-endl>`, "Yes"
+   :doc:`performance-const-ref-copy <performance/const-ref-copy>`,
    :doc:`performance-enum-size <performance/enum-size>`,
    :doc:`performance-faster-string-find <performance/faster-string-find>`, 
"Yes"
    :doc:`performance-for-range-copy <performance/for-range-copy>`, "Yes"
diff --git 
a/clang-tools-extra/docs/clang-tidy/checks/performance/const-ref-copy.rst 
b/clang-tools-extra/docs/clang-tidy/checks/performance/const-ref-copy.rst
new file mode 100644
index 0000000000000..20cf04b9735ca
--- /dev/null
+++ b/clang-tools-extra/docs/clang-tidy/checks/performance/const-ref-copy.rst
@@ -0,0 +1,39 @@
+.. title:: clang-tidy - performance-const-ref-copy
+
+performance-const-ref-copy
+==========================
+
+Finds function parameters declared as ``const T&`` that are
+unconditionally copied into a local variable, and suggests
+changing the parameter to pass-by-value with ``std::move``.
+
+When a ``const T&`` parameter is always copied, passing by value
+instead allows callers with rvalues to avoid the copy entirely
+through move construction.
+
+For example:
+
+.. code-block:: c++
+
+  // Before
+  void process(const std::string &s) {
+    std::string local = s;
+    // use local...
+  }
+
+  // After
+  void process(std::string s) {
+    std::string local = std::move(s);
+    // use local...
+  }
+
+The check only triggers when:
+
+- The parameter type is a non-trivially-copyable class or struct.
+- The type has a non-deleted move constructor.
+- The parameter is copied via copy construction into a local
+  variable.
+
+This check does not handle constructor member initializer lists,
+as that case is covered by :doc:`modernize-pass-by-value
+<../modernize/pass-by-value>`.
diff --git 
a/clang-tools-extra/test/clang-tidy/checkers/performance/const-ref-copy.cpp 
b/clang-tools-extra/test/clang-tidy/checkers/performance/const-ref-copy.cpp
new file mode 100644
index 0000000000000..c879ae3fa06e4
--- /dev/null
+++ b/clang-tools-extra/test/clang-tidy/checkers/performance/const-ref-copy.cpp
@@ -0,0 +1,84 @@
+// RUN: %check_clang_tidy %s performance-const-ref-copy %t
+
+namespace std {
+template <typename T>
+class basic_string {
+public:
+  basic_string();
+  basic_string(const basic_string &);
+  basic_string(basic_string &&);
+  basic_string &operator=(const basic_string &);
+  basic_string &operator=(basic_string &&);
+  ~basic_string();
+  const char *c_str() const;
+  int size() const;
+};
+using string = basic_string<char>;
+
+template <typename T>
+class vector {
+public:
+  vector();
+  vector(const vector &);
+  vector(vector &&);
+  vector &operator=(const vector &);
+  vector &operator=(vector &&);
+  ~vector();
+  int size() const;
+};
+} // namespace std
+
+// Positive: const string& copied into local variable.
+void take_string(const std::string &s) {
+  // CHECK-MESSAGES: :[[@LINE-1]]:37: warning: parameter 's' is copied into 
local variable 'copy'; consider passing by value and using 'std::move' 
[performance-const-ref-copy]
+  std::string copy = s;
+  // CHECK-MESSAGES: :[[@LINE-1]]:15: note: copy is here
+  copy.c_str();
+}
+
+// Positive: const vector& copied into local.
+void take_vector(const std::vector<int> &v) {
+  // CHECK-MESSAGES: :[[@LINE-1]]:42: warning: parameter 'v' is copied into 
local variable 'local'; consider passing by value and using 'std::move' 
[performance-const-ref-copy]
+  std::vector<int> local = v;
+  // CHECK-MESSAGES: :[[@LINE-1]]:20: note: copy is here
+  local.size();
+}
+
+// Negative: trivially copyable type.
+struct Trivial {
+  int x;
+  int y;
+};
+
+void take_trivial(const Trivial &t) {
+  Trivial copy = t;
+}
+
+// Negative: no copy, just using the reference.
+void use_ref(const std::string &s) {
+  s.c_str();
+}
+
+// Negative: type has no move constructor (deleted).
+struct NoMove {
+  NoMove();
+  NoMove(const NoMove &);
+  NoMove &operator=(const NoMove &);
+  ~NoMove();
+  NoMove(NoMove &&) = delete;
+};
+
+void take_nomove(const NoMove &n) {
+  NoMove copy = n;
+}
+
+// Negative: not a const ref parameter, just a local.
+void local_copy() {
+  std::string a;
+  std::string b = a;
+}
+
+// Negative: parameter is not const ref.
+void take_value(std::string s) {
+  std::string copy = s;
+}

_______________________________________________
cfe-commits mailing list
[email protected]
https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits

Reply via email to