[clang-tools-extra] [clang-tidy] Add new check bugprone-capture-this-by-field (PR #130297)

Congcong Cai via cfe-commits cfe-commits at lists.llvm.org
Fri Mar 7 08:04:38 PST 2025


https://github.com/HerrCai0907 created https://github.com/llvm/llvm-project/pull/130297


Finds lambda captures that capture the ``this`` pointer and store it as class
members without handle the copy and move constructors and the assignments.

Capture this in a lambda and store it as a class member is dangerous because the
lambda can outlive the object it captures. Especially when the object is copied
or moved, the captured ``this`` pointer will be implicitly propagated to the
new object. Most of the time, people will believe that the captured ``this``
pointer points to the new object, which will lead to bugs.




>From 8ef214f6c78d710dbd9c74b06c7c637baf93e527 Mon Sep 17 00:00:00 2001
From: Congcong Cai <congcongcai0907 at 163.com>
Date: Sat, 8 Mar 2025 00:03:39 +0800
Subject: [PATCH] [clang-tidy] Add new check bugprone-capture-this-by-field

---
 .../bugprone/BugproneTidyModule.cpp           |   3 +
 .../clang-tidy/bugprone/CMakeLists.txt        |   1 +
 .../bugprone/CaptureThisByFieldCheck.cpp      |  99 +++++++++++++
 .../bugprone/CaptureThisByFieldCheck.h        |  39 +++++
 clang-tools-extra/docs/ReleaseNotes.rst       |   6 +
 .../checks/bugprone/capture-this-by-field.rst |  28 ++++
 .../docs/clang-tidy/checks/list.rst           |   1 +
 .../bugprone/capture-this-by-field.cpp        | 133 ++++++++++++++++++
 8 files changed, 310 insertions(+)
 create mode 100644 clang-tools-extra/clang-tidy/bugprone/CaptureThisByFieldCheck.cpp
 create mode 100644 clang-tools-extra/clang-tidy/bugprone/CaptureThisByFieldCheck.h
 create mode 100644 clang-tools-extra/docs/clang-tidy/checks/bugprone/capture-this-by-field.rst
 create mode 100644 clang-tools-extra/test/clang-tidy/checkers/bugprone/capture-this-by-field.cpp

diff --git a/clang-tools-extra/clang-tidy/bugprone/BugproneTidyModule.cpp b/clang-tools-extra/clang-tidy/bugprone/BugproneTidyModule.cpp
index 0a3376949b6e5..ee9fa5ef06c7a 100644
--- a/clang-tools-extra/clang-tidy/bugprone/BugproneTidyModule.cpp
+++ b/clang-tools-extra/clang-tidy/bugprone/BugproneTidyModule.cpp
@@ -16,6 +16,7 @@
 #include "BitwisePointerCastCheck.h"
 #include "BoolPointerImplicitConversionCheck.h"
 #include "BranchCloneCheck.h"
+#include "CaptureThisByFieldCheck.h"
 #include "CastingThroughVoidCheck.h"
 #include "ChainedComparisonCheck.h"
 #include "ComparePointerToMemberVirtualFunctionCheck.h"
@@ -118,6 +119,8 @@ class BugproneModule : public ClangTidyModule {
     CheckFactories.registerCheck<BoolPointerImplicitConversionCheck>(
         "bugprone-bool-pointer-implicit-conversion");
     CheckFactories.registerCheck<BranchCloneCheck>("bugprone-branch-clone");
+    CheckFactories.registerCheck<CaptureThisByFieldCheck>(
+        "bugprone-capture-this-by-field");
     CheckFactories.registerCheck<CastingThroughVoidCheck>(
         "bugprone-casting-through-void");
     CheckFactories.registerCheck<ChainedComparisonCheck>(
diff --git a/clang-tools-extra/clang-tidy/bugprone/CMakeLists.txt b/clang-tools-extra/clang-tidy/bugprone/CMakeLists.txt
index dab139b77c770..4d1d50c4ea2a0 100644
--- a/clang-tools-extra/clang-tidy/bugprone/CMakeLists.txt
+++ b/clang-tools-extra/clang-tidy/bugprone/CMakeLists.txt
@@ -12,6 +12,7 @@ add_clang_library(clangTidyBugproneModule STATIC
   BoolPointerImplicitConversionCheck.cpp
   BranchCloneCheck.cpp
   BugproneTidyModule.cpp
+  CaptureThisByFieldCheck.cpp
   CastingThroughVoidCheck.cpp
   ChainedComparisonCheck.cpp
   ComparePointerToMemberVirtualFunctionCheck.cpp
diff --git a/clang-tools-extra/clang-tidy/bugprone/CaptureThisByFieldCheck.cpp b/clang-tools-extra/clang-tidy/bugprone/CaptureThisByFieldCheck.cpp
new file mode 100644
index 0000000000000..1f0f68acf335f
--- /dev/null
+++ b/clang-tools-extra/clang-tidy/bugprone/CaptureThisByFieldCheck.cpp
@@ -0,0 +1,99 @@
+//===--- CaptureThisByFieldCheck.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 "CaptureThisByFieldCheck.h"
+#include "clang/AST/DeclCXX.h"
+#include "clang/ASTMatchers/ASTMatchFinder.h"
+#include "clang/ASTMatchers/ASTMatchers.h"
+#include "clang/ASTMatchers/ASTMatchersMacros.h"
+
+using namespace clang::ast_matchers;
+
+namespace clang::tidy::bugprone {
+
+namespace {
+
+AST_MATCHER(CXXRecordDecl, correctHandleCaptureThisLambda) {
+  // unresolved
+  if (Node.needsOverloadResolutionForCopyConstructor() &&
+      Node.needsImplicitCopyConstructor())
+    return false;
+  if (Node.needsOverloadResolutionForMoveConstructor() &&
+      Node.needsImplicitMoveConstructor())
+    return false;
+  if (Node.needsOverloadResolutionForCopyAssignment() &&
+      Node.needsImplicitCopyAssignment())
+    return false;
+  if (Node.needsOverloadResolutionForMoveAssignment() &&
+      Node.needsImplicitMoveAssignment())
+    return false;
+  // default but not deleted
+  if (Node.hasSimpleCopyConstructor())
+    return false;
+  if (Node.hasSimpleMoveConstructor())
+    return false;
+  if (Node.hasSimpleCopyAssignment())
+    return false;
+  if (Node.hasSimpleMoveAssignment())
+    return false;
+
+  for (CXXConstructorDecl const *C : Node.ctors()) {
+    if (C->isCopyOrMoveConstructor() && C->isDefaulted() && !C->isDeleted())
+      return false;
+  }
+  for (CXXMethodDecl const *M : Node.methods()) {
+    if (M->isCopyAssignmentOperator() && M->isDefaulted() && !M->isDeleted())
+      return false;
+    if (M->isMoveAssignmentOperator() && M->isDefaulted() && !M->isDeleted())
+      return false;
+  }
+  // FIXME: find ways to identifier correct handle capture this lambda
+  return true;
+}
+
+} // namespace
+
+void CaptureThisByFieldCheck::registerMatchers(MatchFinder *Finder) {
+  auto IsStdFunctionField =
+      fieldDecl(hasType(cxxRecordDecl(hasName("::std::function"))))
+          .bind("field");
+  auto CaptureThis = lambdaCapture(anyOf(
+      // [this]
+      capturesThis(),
+      // [self = this]
+      capturesVar(varDecl(hasInitializer(cxxThisExpr())))));
+  auto IsInitWithLambda = cxxConstructExpr(hasArgument(
+      0,
+      lambdaExpr(hasAnyCapture(CaptureThis.bind("capture"))).bind("lambda")));
+  Finder->addMatcher(
+      cxxRecordDecl(
+          has(cxxConstructorDecl(
+              unless(isCopyConstructor()), unless(isMoveConstructor()),
+              hasAnyConstructorInitializer(cxxCtorInitializer(
+                  isMemberInitializer(), forField(IsStdFunctionField),
+                  withInitializer(IsInitWithLambda))))),
+          unless(correctHandleCaptureThisLambda())),
+      this);
+}
+
+void CaptureThisByFieldCheck::check(const MatchFinder::MatchResult &Result) {
+  const auto *Capture = Result.Nodes.getNodeAs<LambdaCapture>("capture");
+  const auto *Lambda = Result.Nodes.getNodeAs<LambdaExpr>("lambda");
+  const auto *Field = Result.Nodes.getNodeAs<FieldDecl>("field");
+  diag(Lambda->getBeginLoc(),
+       "using lambda expressions to capture this and storing it in class "
+       "member will cause potential variable lifetime issue when the class "
+       "instance is moved or copied")
+      << Capture->getLocation();
+  diag(Field->getLocation(),
+       "'std::function' that stores captured this and becomes invalid during "
+       "copying or moving",
+       DiagnosticIDs::Note);
+}
+
+} // namespace clang::tidy::bugprone
diff --git a/clang-tools-extra/clang-tidy/bugprone/CaptureThisByFieldCheck.h b/clang-tools-extra/clang-tidy/bugprone/CaptureThisByFieldCheck.h
new file mode 100644
index 0000000000000..72c0a540a7743
--- /dev/null
+++ b/clang-tools-extra/clang-tidy/bugprone/CaptureThisByFieldCheck.h
@@ -0,0 +1,39 @@
+//===--- CaptureThisByFieldCheck.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_BUGPRONE_CAPTURETHISBYFIELDCHECK_H
+#define LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_BUGPRONE_CAPTURETHISBYFIELDCHECK_H
+
+#include "../ClangTidyCheck.h"
+#include "clang/AST/ASTTypeTraits.h"
+#include <optional>
+
+namespace clang::tidy::bugprone {
+
+/// Finds lambda captures that capture the ``this`` pointer and store it as class
+/// members without handle the copy and move constructors and the assignments.
+///
+/// For the user-facing documentation see:
+/// http://clang.llvm.org/extra/clang-tidy/checks/bugprone/capture-this-by-field.html
+class CaptureThisByFieldCheck : public ClangTidyCheck {
+public:
+  CaptureThisByFieldCheck(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;
+  }
+  std::optional<TraversalKind> getCheckTraversalKind() const override {
+    return TraversalKind::TK_IgnoreUnlessSpelledInSource;
+  }
+};
+
+} // namespace clang::tidy::bugprone
+
+#endif // LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_BUGPRONE_CAPTURETHISBYFIELDCHECK_H
diff --git a/clang-tools-extra/docs/ReleaseNotes.rst b/clang-tools-extra/docs/ReleaseNotes.rst
index 71edb704b49d6..002f60dfb6fa8 100644
--- a/clang-tools-extra/docs/ReleaseNotes.rst
+++ b/clang-tools-extra/docs/ReleaseNotes.rst
@@ -91,6 +91,12 @@ Improvements to clang-tidy
 New checks
 ^^^^^^^^^^
 
+- New :doc:`bugprone-capture-this-by-field
+  <clang-tidy/checks/bugprone/capture-this-by-field>` check.
+
+  Finds lambda captures that capture the ``this`` pointer and store it as class
+  members without handle the copy and move constructors and the assignments.
+
 - New :doc:`bugprone-unintended-char-ostream-output
   <clang-tidy/checks/bugprone/unintended-char-ostream-output>` check.
 
diff --git a/clang-tools-extra/docs/clang-tidy/checks/bugprone/capture-this-by-field.rst b/clang-tools-extra/docs/clang-tidy/checks/bugprone/capture-this-by-field.rst
new file mode 100644
index 0000000000000..c837ce5cb6d68
--- /dev/null
+++ b/clang-tools-extra/docs/clang-tidy/checks/bugprone/capture-this-by-field.rst
@@ -0,0 +1,28 @@
+.. title:: clang-tidy - bugprone-capture-this-by-field
+
+bugprone-capture-this-by-field
+==============================
+
+Finds lambda captures that capture the ``this`` pointer and store it as class
+members without handle the copy and move constructors and the assignments.
+
+Capture this in a lambda and store it as a class member is dangerous because the
+lambda can outlive the object it captures. Especially when the object is copied
+or moved, the captured ``this`` pointer will be implicitly propagated to the
+new object. Most of the time, people will believe that the captured ``this``
+pointer points to the new object, which will lead to bugs.
+
+
+.. code-block:: c++
+
+  struct C {
+    C() : Captured([this]() -> C const * { return this; }) {}
+    std::function<C const *()> Captured;
+  };
+
+  void foo() {
+    C v1{};
+    C v2 = v1; // v2.Captured capture v1's this pointer
+    assert(v2.Captured() == v1.Captured()); // v2.Captured capture v1's this pointer
+    assert(v2.Captured() == &v2); // assertion failed.
+  }
diff --git a/clang-tools-extra/docs/clang-tidy/checks/list.rst b/clang-tools-extra/docs/clang-tidy/checks/list.rst
index 5f03ef72cc603..6bef286f28100 100644
--- a/clang-tools-extra/docs/clang-tidy/checks/list.rst
+++ b/clang-tools-extra/docs/clang-tidy/checks/list.rst
@@ -84,6 +84,7 @@ Clang-Tidy Checks
    :doc:`bugprone-bitwise-pointer-cast <bugprone/bitwise-pointer-cast>`,
    :doc:`bugprone-bool-pointer-implicit-conversion <bugprone/bool-pointer-implicit-conversion>`, "Yes"
    :doc:`bugprone-branch-clone <bugprone/branch-clone>`,
+   :doc:`bugprone-capture-this-by-field <bugprone/capture-this-by-field>`,
    :doc:`bugprone-casting-through-void <bugprone/casting-through-void>`,
    :doc:`bugprone-chained-comparison <bugprone/chained-comparison>`,
    :doc:`bugprone-compare-pointer-to-member-virtual-function <bugprone/compare-pointer-to-member-virtual-function>`,
diff --git a/clang-tools-extra/test/clang-tidy/checkers/bugprone/capture-this-by-field.cpp b/clang-tools-extra/test/clang-tidy/checkers/bugprone/capture-this-by-field.cpp
new file mode 100644
index 0000000000000..4ffd6c7ade51e
--- /dev/null
+++ b/clang-tools-extra/test/clang-tidy/checkers/bugprone/capture-this-by-field.cpp
@@ -0,0 +1,133 @@
+// RUN: %check_clang_tidy -std=c++11-or-later %s bugprone-capture-this-by-field %t
+
+namespace std {
+
+template<class Fn>
+class function;
+
+template<class R, class ...Args>
+class function<R(Args...)> {
+public:
+  function() noexcept;
+  template<class F> function(F &&);
+};
+
+} // namespace std
+
+struct Basic {
+  Basic() : Captured([this]() { static_cast<void>(this); }) {}
+  // CHECK-MESSAGES: :[[@LINE-1]]:22: warning: using lambda expressions to capture this and storing it in class member
+  std::function<void()> Captured;
+  // CHECK-MESSAGES: :[[@LINE-1]]:25: note: 'std::function' that stores captured this
+};
+
+struct AssignCapture {
+  AssignCapture() : Captured([Self = this]() { static_cast<void>(Self); }) {}
+  // CHECK-MESSAGES: :[[@LINE-1]]:30: warning: using lambda expressions to capture this and storing it in class member
+  std::function<void()> Captured;
+  // CHECK-MESSAGES: :[[@LINE-1]]:25: note: 'std::function' that stores captured this
+};
+
+struct DeleteMoveAndCopy {
+  DeleteMoveAndCopy() : Captured([this]() { static_cast<void>(this); }) {}
+  DeleteMoveAndCopy(DeleteMoveAndCopy const&) = delete;
+  DeleteMoveAndCopy(DeleteMoveAndCopy &&) = delete;
+  DeleteMoveAndCopy& operator=(DeleteMoveAndCopy const&) = delete;
+  DeleteMoveAndCopy& operator=(DeleteMoveAndCopy &&) = delete;
+  std::function<void()> Captured;
+};
+
+struct DeleteCopyImplicitDisabledMove {
+  DeleteCopyImplicitDisabledMove() : Captured([this]() { static_cast<void>(this); }) {}
+  DeleteCopyImplicitDisabledMove(DeleteCopyImplicitDisabledMove const&) = delete;
+  DeleteCopyImplicitDisabledMove& operator=(DeleteCopyImplicitDisabledMove const&) = delete;
+  std::function<void()> Captured;
+};
+
+struct DeleteCopyDefaultMove {
+  DeleteCopyDefaultMove() : Captured([this]() { static_cast<void>(this); }) {}
+  // CHECK-MESSAGES: :[[@LINE-1]]:38: warning: using lambda expressions to capture this and storing it in class member
+  DeleteCopyDefaultMove(DeleteCopyDefaultMove const&) = delete;
+  DeleteCopyDefaultMove(DeleteCopyDefaultMove &&) = default;
+  DeleteCopyDefaultMove& operator=(DeleteCopyDefaultMove const&) = delete;
+  DeleteCopyDefaultMove& operator=(DeleteCopyDefaultMove &&) = default;
+  std::function<void()> Captured;
+  // CHECK-MESSAGES: :[[@LINE-1]]:25: note: 'std::function' that stores captured this
+};
+
+struct DeleteMoveDefaultCopy {
+  DeleteMoveDefaultCopy() : Captured([this]() { static_cast<void>(this); }) {}
+  // CHECK-MESSAGES: :[[@LINE-1]]:38: warning: using lambda expressions to capture this and storing it in class member
+  DeleteMoveDefaultCopy(DeleteMoveDefaultCopy const&) = default;
+  DeleteMoveDefaultCopy(DeleteMoveDefaultCopy &&) = delete;
+  DeleteMoveDefaultCopy& operator=(DeleteMoveDefaultCopy const&) = default;
+  DeleteMoveDefaultCopy& operator=(DeleteMoveDefaultCopy &&) = delete;
+  std::function<void()> Captured;
+  // CHECK-MESSAGES: :[[@LINE-1]]:25: note: 'std::function' that stores captured this
+};
+
+struct DeleteCopyMoveBase {
+  DeleteCopyMoveBase() = default;
+  DeleteCopyMoveBase(DeleteCopyMoveBase const&) = delete;
+  DeleteCopyMoveBase(DeleteCopyMoveBase &&) = delete;
+  DeleteCopyMoveBase& operator=(DeleteCopyMoveBase const&) = delete;
+  DeleteCopyMoveBase& operator=(DeleteCopyMoveBase &&) = delete;
+};
+
+struct Inherit : DeleteCopyMoveBase {
+  Inherit() : DeleteCopyMoveBase{}, Captured([this]() { static_cast<void>(this); }) {}
+  std::function<void()> Captured;
+};
+
+struct UserDefinedCopyMove {
+  UserDefinedCopyMove() : Captured([this]() { static_cast<void>(this); }) {}
+  UserDefinedCopyMove(UserDefinedCopyMove const&);
+  UserDefinedCopyMove(UserDefinedCopyMove &&);
+  UserDefinedCopyMove& operator=(UserDefinedCopyMove const&);
+  UserDefinedCopyMove& operator=(UserDefinedCopyMove &&);
+  std::function<void()> Captured;
+};
+
+struct UserDefinedCopyMoveWithDefault1 {
+  UserDefinedCopyMoveWithDefault1() : Captured([this]() { static_cast<void>(this); }) {}
+  // CHECK-MESSAGES: :[[@LINE-1]]:48: warning: using lambda expressions to capture this and storing it in class member
+  UserDefinedCopyMoveWithDefault1(UserDefinedCopyMoveWithDefault1 const&) = default;
+  UserDefinedCopyMoveWithDefault1(UserDefinedCopyMoveWithDefault1 &&);
+  UserDefinedCopyMoveWithDefault1& operator=(UserDefinedCopyMoveWithDefault1 const&);
+  UserDefinedCopyMoveWithDefault1& operator=(UserDefinedCopyMoveWithDefault1 &&);
+  std::function<void()> Captured;
+  // CHECK-MESSAGES: :[[@LINE-1]]:25: note: 'std::function' that stores captured this
+};
+
+struct UserDefinedCopyMoveWithDefault2 {
+  UserDefinedCopyMoveWithDefault2() : Captured([this]() { static_cast<void>(this); }) {}
+  // CHECK-MESSAGES: :[[@LINE-1]]:48: warning: using lambda expressions to capture this and storing it in class member
+  UserDefinedCopyMoveWithDefault2(UserDefinedCopyMoveWithDefault2 const&);
+  UserDefinedCopyMoveWithDefault2(UserDefinedCopyMoveWithDefault2 &&) = default;
+  UserDefinedCopyMoveWithDefault2& operator=(UserDefinedCopyMoveWithDefault2 const&);
+  UserDefinedCopyMoveWithDefault2& operator=(UserDefinedCopyMoveWithDefault2 &&);
+  std::function<void()> Captured;
+  // CHECK-MESSAGES: :[[@LINE-1]]:25: note: 'std::function' that stores captured this
+};
+
+struct UserDefinedCopyMoveWithDefault3 {
+  UserDefinedCopyMoveWithDefault3() : Captured([this]() { static_cast<void>(this); }) {}
+  // CHECK-MESSAGES: :[[@LINE-1]]:48: warning: using lambda expressions to capture this and storing it in class member
+  UserDefinedCopyMoveWithDefault3(UserDefinedCopyMoveWithDefault3 const&);
+  UserDefinedCopyMoveWithDefault3(UserDefinedCopyMoveWithDefault3 &&);
+  UserDefinedCopyMoveWithDefault3& operator=(UserDefinedCopyMoveWithDefault3 const&) = default;
+  UserDefinedCopyMoveWithDefault3& operator=(UserDefinedCopyMoveWithDefault3 &&);
+  std::function<void()> Captured;
+  // CHECK-MESSAGES: :[[@LINE-1]]:25: note: 'std::function' that stores captured this
+};
+
+struct UserDefinedCopyMoveWithDefault4 {
+  UserDefinedCopyMoveWithDefault4() : Captured([this]() { static_cast<void>(this); }) {}
+  // CHECK-MESSAGES: :[[@LINE-1]]:48: warning: using lambda expressions to capture this and storing it in class member
+  UserDefinedCopyMoveWithDefault4(UserDefinedCopyMoveWithDefault4 const&);
+  UserDefinedCopyMoveWithDefault4(UserDefinedCopyMoveWithDefault4 &&);
+  UserDefinedCopyMoveWithDefault4& operator=(UserDefinedCopyMoveWithDefault4 const&);
+  UserDefinedCopyMoveWithDefault4& operator=(UserDefinedCopyMoveWithDefault4 &&) = default;
+  std::function<void()> Captured;
+  // CHECK-MESSAGES: :[[@LINE-1]]:25: note: 'std::function' that stores captured this
+};



More information about the cfe-commits mailing list