Author: Yuta Nakamura
Date: 2026-06-02T01:58:55Z
New Revision: 8ce6c5ff2c134414d8e1815bdc122699abd89954

URL: 
https://github.com/llvm/llvm-project/commit/8ce6c5ff2c134414d8e1815bdc122699abd89954
DIFF: 
https://github.com/llvm/llvm-project/commit/8ce6c5ff2c134414d8e1815bdc122699abd89954.diff

LOG: [clang-tidy] Fix false positive in bugprone-use-after-move for std::tie 
(#192895)

std::tie(a, b) = expr reinitializes all variables passed to std::tie
because the tuple assignment operator writes back through the stored
references. The check was not recognizing this pattern, causing a false
positive on the second std::tie assignment in loops like:

  std::tie(a, b) = foo(std::move(a), std::move(b));
  std::tie(a, b) = foo(std::move(a), std::move(b)); // false positive

Add std::tie assignment as a reinitialization case in
makeReinitMatcher().

Fixes #136105.

---

**AI Disclosure:** Claude (Anthropic) was used to assist in diagnosing
the CI test failure and identifying the off-by-one line number in the
CHECK-NOTES.

---------

Co-authored-by: Claude Sonnet 4.6 <[email protected]>
Co-authored-by: Zeyi Xu <[email protected]>

Added: 
    

Modified: 
    clang-tools-extra/clang-tidy/bugprone/UseAfterMoveCheck.cpp
    clang-tools-extra/docs/ReleaseNotes.rst
    clang-tools-extra/docs/clang-tidy/checks/bugprone/use-after-move.rst
    clang-tools-extra/test/clang-tidy/checkers/bugprone/use-after-move.cpp

Removed: 
    


################################################################################
diff  --git a/clang-tools-extra/clang-tidy/bugprone/UseAfterMoveCheck.cpp 
b/clang-tools-extra/clang-tidy/bugprone/UseAfterMoveCheck.cpp
index 399442f52bd33..361be321185df 100644
--- a/clang-tools-extra/clang-tidy/bugprone/UseAfterMoveCheck.cpp
+++ b/clang-tools-extra/clang-tidy/bugprone/UseAfterMoveCheck.cpp
@@ -134,6 +134,15 @@ makeReinitMatcher(const ValueDecl *MovedVariable,
                  // built-in types.
                  binaryOperation(hasOperatorName("="),
                                  
hasLHS(ignoringParenImpCasts(DeclRefMatcher))),
+                 // std::tie() assignment: std::tie(a, b) = expr reinitializes
+                 // all variables passed to std::tie because the tuple
+                 // assignment writes back through the stored references.
+                 binaryOperation(
+                     hasOperatorName("="),
+                     hasLHS(ignoringImplicit(ignoringParenImpCasts(
+                         callExpr(callee(functionDecl(hasName("::std::tie"))),
+                                  hasAnyArgument(ignoringParenImpCasts(
+                                      DeclRefMatcher))))))),
                  // Declaration. We treat this as a type of reinitialization
                  // too, so we don't need to treat it separately.
                  declStmt(hasDescendant(equalsNode(MovedVariable))),

diff  --git a/clang-tools-extra/docs/ReleaseNotes.rst 
b/clang-tools-extra/docs/ReleaseNotes.rst
index 0189e84f8f12f..ba0cea800cd4e 100644
--- a/clang-tools-extra/docs/ReleaseNotes.rst
+++ b/clang-tools-extra/docs/ReleaseNotes.rst
@@ -441,6 +441,11 @@ Changes in existing checks
   - Avoid false positives when moving object is reinitialized via the base
     class's ``operator=``.
 
+  - Fix a false positive when a moved-from variable is reinitialized
+    via a ``std::tie()`` assignment (e.g. ``std::tie(a, b) = f(std::move(a),
+    std::move(b))``). The tuple assignment writes back through the stored
+    references, which fully reinitializes the captured variables.
+
 - Improved :doc:`cert-err33-c
   <clang-tidy/checks/cert/err33-c>` check by not inheriting
   `CheckedReturnTypes` option from :doc:`bugprone-unused-return-value

diff  --git 
a/clang-tools-extra/docs/clang-tidy/checks/bugprone/use-after-move.rst 
b/clang-tools-extra/docs/clang-tidy/checks/bugprone/use-after-move.rst
index da72f742b38d0..4e746b2633c07 100644
--- a/clang-tools-extra/docs/clang-tidy/checks/bugprone/use-after-move.rst
+++ b/clang-tools-extra/docs/clang-tidy/checks/bugprone/use-after-move.rst
@@ -233,6 +233,11 @@ The check considers a variable to be reinitialized in the 
following cases:
   - A member function marked with the ``[[clang::reinitializes]]`` attribute is
     called on the variable.
 
+  - The variable is passed as an argument to ``std::tie`` on the left-hand
+    side of an assignment (e.g. ``std::tie(a, b) = f(...)``). The tuple
+    assignment operator writes back through the stored references, which
+    reinitializes each named variable.
+
 If the variable in question is a struct and an individual member variable of
 that struct is written to, the check does not consider this to be a
 reinitialization -- even if, eventually, all member variables of the struct are

diff  --git 
a/clang-tools-extra/test/clang-tidy/checkers/bugprone/use-after-move.cpp 
b/clang-tools-extra/test/clang-tidy/checkers/bugprone/use-after-move.cpp
index d4e78d359b654..0844aa32825f6 100644
--- a/clang-tools-extra/test/clang-tidy/checkers/bugprone/use-after-move.cpp
+++ b/clang-tools-extra/test/clang-tidy/checkers/bugprone/use-after-move.cpp
@@ -17,6 +17,7 @@
 #include <map>
 #include <set>
 #include <string>
+#include <tuple>
 #include <unordered_map>
 #include <unordered_set>
 #include <utility>
@@ -1814,3 +1815,129 @@ namespace GH62206 {
     (d) = b; // Should not warn
   }
 } // namespace GH62206
+
+
+std::pair<std::string, std::string> makeStringPair(std::string a,
+                                                   std::string b);
+
+void stdTieIsReinit() {
+  std::string a, b;
+  std::move(a);
+  std::move(b);
+  std::tie(a, b) = makeStringPair("x", "y");
+  a.size();
+  b.size();
+}
+
+void stdTieWithIgnore() {
+  std::string a;
+  std::move(a);
+  std::tie(a, std::ignore) = makeStringPair("x", "y");
+  a.size();
+}
+
+void stdTieIgnoreFlipped() {
+  std::string a;
+  std::move(a);
+  std::tie(std::ignore, a) = makeStringPair("x", "y");
+  a.size();
+}
+
+void stdTieThreeVars() {
+  std::string a, b, c;
+  std::move(a);
+  std::move(b);
+  std::move(c);
+  std::tie(a, b, c) =
+      std::make_tuple(std::string("x"), std::string("y"), std::string("z"));
+  a.size();
+  b.size();
+  c.size();
+}
+
+void stdTiePartialReinit() {
+  std::string a, b, c;
+  std::move(a);
+  std::move(b);
+  std::move(c);
+  std::tie(b) = std::make_tuple(std::string("y"));
+  b.size();
+  std::tie(c, b, std::ignore) =
+      std::make_tuple(std::string("x"), std::string("y"), std::string("z"));
+  b.size();
+  c.size();
+  a.size();
+  // CHECK-NOTES: [[@LINE-1]]:3: warning: 'a' used after it was moved
+  // CHECK-NOTES: [[@LINE-11]]:3: note: move occurred here
+}
+
+void stdTieInLoop() {
+  std::string a, b;
+  while (true) {
+    std::tie(a, b) = makeStringPair(std::move(a), std::move(b));
+    std::tie(a, b) = makeStringPair(std::move(a), std::move(b));
+  }
+}
+
+template <typename T>
+void stdTieReinitInTemplate() {
+  T a, b;
+  std::move(a);
+  std::move(b);
+  std::tie(a, b) = std::make_tuple(T(), T());
+  a.size();
+  b.size();
+}
+
+template <typename T>
+void stdTiePartialReinitInTemplate() {
+  T a, b;
+  std::move(a);
+  std::tie(b) = std::make_tuple(T());
+  b.size();
+  a.size();
+  // CHECK-NOTES: [[@LINE-1]]:3: warning: 'a' used after it was moved
+  // CHECK-NOTES: [[@LINE-5]]:3: note: move occurred here
+}
+
+void callStdTieTemplateTests() {
+  stdTieReinitInTemplate<std::string>();
+  stdTiePartialReinitInTemplate<std::string>();
+}
+
+void stdTieParenthesized() {
+  // Parenthesized std::tie on the LHS still reinitializes captured variables.
+  std::string a, b;
+  std::move(a);
+  std::move(b);
+  (std::tie(a, b)) = makeStringPair("x", "y"); // no-warning: both 
reinitialized
+  a.size();
+  b.size();
+}
+
+void stdTieParenthesizedWithIgnore() {
+  // Parenthesized std::tie with std::ignore still reinitializes named 
variables.
+  std::string a;
+  std::move(a);
+  (std::tie(a, std::ignore)) = makeStringPair("x", "y"); // no-warning: a 
reinitialized
+  a.size();
+}
+
+void stdTieParenthesizedIgnoreFlipped() {
+  // std::ignore in first position inside parenthesized std::tie.
+  std::string a;
+  std::move(a);
+  (std::tie(std::ignore, a)) = makeStringPair("x", "y"); // no-warning: a 
reinitialized
+  a.size();
+}
+
+void stdTieParenthesizedPartialReinit() {
+  // Parenthesized std::tie only reinitializes variables named in the call.
+  std::string a, b;
+  std::move(a);
+  (std::tie(b)) = std::make_tuple(std::string("y")); // reinitializes b, not a
+  b.size(); // no-warning: b was reinitialized
+  a.size();
+  // CHECK-NOTES: [[@LINE-1]]:3: warning: 'a' used after it was moved
+  // CHECK-NOTES: [[@LINE-5]]:3: note: move occurred here
+}


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

Reply via email to