Skip to content

Conversation

flovent
Copy link
Contributor

@flovent flovent commented Sep 14, 2025

This check detect code which can be transfered to c++17 structured binding, and provides fix-it.

Limitations:

  1. Ignore variables with attributes or qualifiers except const and & since it's not very common:
static auto pair = getPair(); // no warning from this check
static int b = pair.first;
static int c = pair.second;
  1. Some possibly convertable case:

(1)

const auto& results = mapping.try_emplace("hello!"); 
const iterator& it = results.first;
bool succeed = results.second;
// No change for succeed

In theory this can be transfered to structured binding:

const auto& [it, succeed] = mapping.try_emplace("hello!"); 

But it's needed to check whether succeed is changed after definition.
(2)

const auto results = mapping.try_emplace("hello!");
if (results.second) {
    handle_inserted(results.first);
}
// no name deduced

That's not checked too.

It's coming from #138735, but leaves some unhanlded cases mentioned above.

@llvmbot
Copy link
Member

llvmbot commented Sep 14, 2025

@llvm/pr-subscribers-clang-tools-extra

@llvm/pr-subscribers-clang-tidy

Author: None (flovent)

Changes

This check detect code which can be transfered to c++17 structured binding, and provides fix-it.

Limitations:

  1. Ignore variables with attributes or qualifiers except const and & since it's not very common:
static auto pair = getPair(); // no warning from this check
static int b = pair.first;
static int c = pair.second;
  1. Some possibly convertable case:

(1)

const auto& results = mapping.try_emplace("hello!"); 
const iterator& it = results.first;
bool succeed = results.second;
// No change for succeed

In theory this can be transfered to structured binding:

const auto& [it, succeed] = mapping.try_emplace("hello!"); 

But it's needed to check whether succeed is changed after definition.
(2)

const auto results = mapping.try_emplace("hello!");
if (results.second) {
    handle_inserted(results.first);
}
// no name deduced

That's not checked too.

It's coming from #138735, but leaves some unhanlded cases mentioned above.


Patch is 33.29 KiB, truncated to 20.00 KiB below, full version: https://github.com/llvm/llvm-project/pull/158462.diff

11 Files Affected:

  • (modified) clang-tools-extra/clang-tidy/modernize/CMakeLists.txt (+1)
  • (modified) clang-tools-extra/clang-tidy/modernize/ModernizeTidyModule.cpp (+3)
  • (added) clang-tools-extra/clang-tidy/modernize/UseStructuredBindingCheck.cpp (+419)
  • (added) clang-tools-extra/clang-tidy/modernize/UseStructuredBindingCheck.h (+36)
  • (modified) clang-tools-extra/docs/ReleaseNotes.rst (+6)
  • (modified) clang-tools-extra/docs/clang-tidy/checks/list.rst (+1)
  • (added) clang-tools-extra/docs/clang-tidy/checks/modernize/use-structured-binding.rst (+58)
  • (added) clang-tools-extra/test/clang-tidy/checkers/modernize/Inputs/use-structured-binding/fake_std_pair_tuple.h (+23)
  • (added) clang-tools-extra/test/clang-tidy/checkers/modernize/use-structured-binding-custom.cpp (+32)
  • (added) clang-tools-extra/test/clang-tidy/checkers/modernize/use-structured-binding-skip-lambda-capture-in-cxx17.cpp (+67)
  • (added) clang-tools-extra/test/clang-tidy/checkers/modernize/use-structured-binding.cpp (+216)
diff --git a/clang-tools-extra/clang-tidy/modernize/CMakeLists.txt b/clang-tools-extra/clang-tidy/modernize/CMakeLists.txt
index 619a27b2f9bb6..094f0a72b1570 100644
--- a/clang-tools-extra/clang-tidy/modernize/CMakeLists.txt
+++ b/clang-tools-extra/clang-tidy/modernize/CMakeLists.txt
@@ -47,6 +47,7 @@ add_clang_library(clangTidyModernizeModule STATIC
   UseStdFormatCheck.cpp
   UseStdNumbersCheck.cpp
   UseStdPrintCheck.cpp
+  UseStructuredBindingCheck.cpp
   UseTrailingReturnTypeCheck.cpp
   UseTransparentFunctorsCheck.cpp
   UseUncaughtExceptionsCheck.cpp
diff --git a/clang-tools-extra/clang-tidy/modernize/ModernizeTidyModule.cpp b/clang-tools-extra/clang-tidy/modernize/ModernizeTidyModule.cpp
index fdf38bc4b6308..a79908500e904 100644
--- a/clang-tools-extra/clang-tidy/modernize/ModernizeTidyModule.cpp
+++ b/clang-tools-extra/clang-tidy/modernize/ModernizeTidyModule.cpp
@@ -48,6 +48,7 @@
 #include "UseStdFormatCheck.h"
 #include "UseStdNumbersCheck.h"
 #include "UseStdPrintCheck.h"
+#include "UseStructuredBindingCheck.h"
 #include "UseTrailingReturnTypeCheck.h"
 #include "UseTransparentFunctorsCheck.h"
 #include "UseUncaughtExceptionsCheck.h"
@@ -121,6 +122,8 @@ class ModernizeModule : public ClangTidyModule {
     CheckFactories.registerCheck<UseNoexceptCheck>("modernize-use-noexcept");
     CheckFactories.registerCheck<UseNullptrCheck>("modernize-use-nullptr");
     CheckFactories.registerCheck<UseOverrideCheck>("modernize-use-override");
+    CheckFactories.registerCheck<UseStructuredBindingCheck>(
+        "modernize-use-structured-binding");
     CheckFactories.registerCheck<UseTrailingReturnTypeCheck>(
         "modernize-use-trailing-return-type");
     CheckFactories.registerCheck<UseTransparentFunctorsCheck>(
diff --git a/clang-tools-extra/clang-tidy/modernize/UseStructuredBindingCheck.cpp b/clang-tools-extra/clang-tidy/modernize/UseStructuredBindingCheck.cpp
new file mode 100644
index 0000000000000..d6d6ae6cb83b3
--- /dev/null
+++ b/clang-tools-extra/clang-tidy/modernize/UseStructuredBindingCheck.cpp
@@ -0,0 +1,419 @@
+//===--- UseStructuredBindingCheck.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 "UseStructuredBindingCheck.h"
+#include "../utils/DeclRefExprUtils.h"
+#include "../utils/OptionsUtils.h"
+#include "clang/Lex/Lexer.h"
+
+using namespace clang::ast_matchers;
+
+namespace clang::tidy::modernize {
+namespace {
+constexpr const char *DefaultPairTypes = "std::pair";
+constexpr llvm::StringLiteral PairDeclName = "PairVarD";
+constexpr llvm::StringLiteral PairVarTypeName = "PairVarType";
+constexpr llvm::StringLiteral FirstVarDeclName = "FirstVarDecl";
+constexpr llvm::StringLiteral SecondVarDeclName = "SecondVarDecl";
+constexpr llvm::StringLiteral FirstDeclStmtName = "FirstDeclStmt";
+constexpr llvm::StringLiteral SecondDeclStmtName = "SecondDeclStmt";
+constexpr llvm::StringLiteral FirstTypeName = "FirstType";
+constexpr llvm::StringLiteral SecondTypeName = "SecondType";
+constexpr llvm::StringLiteral ScopeBlockName = "ScopeBlock";
+constexpr llvm::StringLiteral StdTieAssignStmtName = "StdTieAssign";
+constexpr llvm::StringLiteral StdTieExprName = "StdTieExpr";
+constexpr llvm::StringLiteral ForRangeStmtName = "ForRangeStmt";
+
+/// What qualifiers and specifiers are used to create structured binding
+/// declaration, it only supports the following four cases now.
+enum TransferType : uint8_t {
+  TT_ByVal,
+  TT_ByConstVal,
+  TT_ByRef,
+  TT_ByConstRef
+};
+
+/// Try to match exactly two VarDecl inside two DeclStmts, and set binding for
+/// the used DeclStmts.
+bool matchTwoVarDecl(const DeclStmt *DS1, const DeclStmt *DS2,
+                     ast_matchers::internal::Matcher<VarDecl> InnerMatcher1,
+                     ast_matchers::internal::Matcher<VarDecl> InnerMatcher2,
+                     internal::ASTMatchFinder *Finder,
+                     internal::BoundNodesTreeBuilder *Builder) {
+  SmallVector<std::pair<const VarDecl *, const DeclStmt *>, 2> Vars;
+  auto CollectVarsInDeclStmt = [&Vars](const DeclStmt *DS) -> bool {
+    if (!DS)
+      return true;
+
+    for (const auto *VD : DS->decls()) {
+      if (Vars.size() == 2)
+        return false;
+
+      if (const auto *Var = dyn_cast<VarDecl>(VD))
+        Vars.emplace_back(Var, DS);
+      else
+        return false;
+    }
+
+    return true;
+  };
+
+  if (!CollectVarsInDeclStmt(DS1) || !CollectVarsInDeclStmt(DS2))
+    return false;
+
+  if (Vars.size() != 2)
+    return false;
+
+  if (InnerMatcher1.matches(*Vars[0].first, Finder, Builder) &&
+      InnerMatcher2.matches(*Vars[1].first, Finder, Builder)) {
+    Builder->setBinding(FirstDeclStmtName,
+                        clang::DynTypedNode::create(*Vars[0].second));
+    if (Vars[0].second != Vars[1].second)
+      Builder->setBinding(SecondDeclStmtName,
+                          clang::DynTypedNode::create(*Vars[1].second));
+    return true;
+  }
+
+  return false;
+}
+
+/// Matches a Stmt whose parent is a CompoundStmt, and which is directly
+/// following two VarDecls matching the inner matcher, at the same time set
+/// binding for the CompoundStmt.
+AST_MATCHER_P2(Stmt, hasPreTwoVarDecl, ast_matchers::internal::Matcher<VarDecl>,
+               InnerMatcher1, ast_matchers::internal::Matcher<VarDecl>,
+               InnerMatcher2) {
+  DynTypedNodeList Parents = Finder->getASTContext().getParents(Node);
+  if (Parents.size() != 1)
+    return false;
+
+  auto *C = Parents[0].get<CompoundStmt>();
+  if (!C)
+    return false;
+
+  const auto I =
+      llvm::find(llvm::make_range(C->body_rbegin(), C->body_rend()), &Node);
+  assert(I != C->body_rend() && "C is parent of Node");
+  if ((I + 1) == C->body_rend())
+    return false;
+
+  const auto *DS2 = dyn_cast<DeclStmt>(*(I + 1));
+  if (!DS2)
+    return false;
+
+  const DeclStmt *DS1 = (!DS2->isSingleDecl() || ((I + 2) == C->body_rend())
+                             ? nullptr
+                             : dyn_cast<DeclStmt>(*(I + 2)));
+
+  if (matchTwoVarDecl(DS1, DS2, InnerMatcher1, InnerMatcher2, Finder,
+                      Builder)) {
+    Builder->setBinding(ScopeBlockName, clang::DynTypedNode::create(*C));
+    return true;
+  }
+
+  return false;
+}
+
+/// Matches a Stmt whose parent is a CompoundStmt, and which is directly
+/// followed by two VarDecls matching the inner matcher, at the same time set
+/// binding for the CompoundStmt.
+AST_MATCHER_P2(Stmt, hasNextTwoVarDecl,
+               ast_matchers::internal::Matcher<VarDecl>, InnerMatcher1,
+               ast_matchers::internal::Matcher<VarDecl>, InnerMatcher2) {
+  DynTypedNodeList Parents = Finder->getASTContext().getParents(Node);
+  if (Parents.size() != 1)
+    return false;
+
+  auto *C = Parents[0].get<CompoundStmt>();
+  if (!C)
+    return false;
+
+  const auto *I = llvm::find(C->body(), &Node);
+  assert(I != C->body_end() && "C is parent of Node");
+  if ((I + 1) == C->body_end())
+    return false;
+
+  if (matchTwoVarDecl(
+          dyn_cast<DeclStmt>(*(I + 1)),
+          ((I + 2) == C->body_end() ? nullptr : dyn_cast<DeclStmt>(*(I + 2))),
+          InnerMatcher1, InnerMatcher2, Finder, Builder)) {
+    Builder->setBinding(ScopeBlockName, clang::DynTypedNode::create(*C));
+    return true;
+  }
+
+  return false;
+}
+
+/// Matches a Stmt whose parent is a CompoundStmt, and there a two VarDecls
+/// matching the inner matcher in the beginning of CompoundStmt.
+AST_MATCHER_P2(CompoundStmt, hasFirstTwoVarDecl,
+               ast_matchers::internal::Matcher<VarDecl>, InnerMatcher1,
+               ast_matchers::internal::Matcher<VarDecl>, InnerMatcher2) {
+  const auto *I = Node.body_begin();
+  if ((I) == Node.body_end())
+    return false;
+
+  return matchTwoVarDecl(
+      dyn_cast<DeclStmt>(*(I)),
+      ((I + 1) == Node.body_end() ? nullptr : dyn_cast<DeclStmt>(*(I + 1))),
+      InnerMatcher1, InnerMatcher2, Finder, Builder);
+}
+
+/// It's not very common to have specifiers for variables used to decompose
+/// a pair, so we ignore these cases.
+AST_MATCHER(VarDecl, hasAnySpecifiersShouldBeIgnored) {
+  return Node.isStaticLocal() || Node.isConstexpr() || Node.hasAttrs() ||
+         Node.isInlineSpecified();
+}
+
+// Ignore nodes inside macros.
+AST_POLYMORPHIC_MATCHER(isInMarco,
+                        AST_POLYMORPHIC_SUPPORTED_TYPES(Stmt, Decl)) {
+  return Node.getBeginLoc().isMacroID() || Node.getEndLoc().isMacroID();
+}
+
+AST_MATCHER_P(Expr, ignoringCopyCtorAndImplicitCast,
+              ast_matchers::internal::Matcher<Expr>, InnerMatcher) {
+  if (const auto *CtorE = dyn_cast<CXXConstructExpr>(&Node)) {
+    if (const auto *CtorD = CtorE->getConstructor();
+        CtorD->isCopyConstructor() && CtorE->getNumArgs() == 1) {
+      return InnerMatcher.matches(*CtorE->getArg(0)->IgnoreImpCasts(), Finder,
+                                  Builder);
+    }
+  }
+
+  return InnerMatcher.matches(*Node.IgnoreImpCasts(), Finder, Builder);
+}
+
+} // namespace
+
+UseStructuredBindingCheck::UseStructuredBindingCheck(StringRef Name,
+                                                     ClangTidyContext *Context)
+    : ClangTidyCheck(Name, Context),
+      PairTypes(utils::options::parseStringList(
+          Options.get("PairTypes", DefaultPairTypes))) {
+  ;
+}
+
+static auto getVarInitWithMemberMatcher(StringRef PairName,
+                                        StringRef MemberName,
+                                        StringRef TypeName,
+                                        StringRef BindingName) {
+  return varDecl(
+             unless(hasAnySpecifiersShouldBeIgnored()), unless(isInMarco()),
+             hasInitializer(
+                 ignoringImpCasts(ignoringCopyCtorAndImplicitCast(memberExpr(
+                     hasObjectExpression(ignoringImpCasts(declRefExpr(
+                         to(equalsBoundNode(std::string(PairName)))))),
+                     member(fieldDecl(hasName(MemberName),
+                                      hasType(qualType().bind(TypeName)))))))))
+      .bind(BindingName);
+}
+
+void UseStructuredBindingCheck::registerMatchers(MatchFinder *Finder) {
+  auto PairType =
+      qualType(unless(isVolatileQualified()),
+               hasUnqualifiedDesugaredType(recordType(
+                   hasDeclaration(cxxRecordDecl(hasAnyName(PairTypes))))));
+
+  auto VarInitWithFirstMember = getVarInitWithMemberMatcher(
+      PairDeclName, "first", FirstTypeName, FirstVarDeclName);
+  auto VarInitWithSecondMember = getVarInitWithMemberMatcher(
+      PairDeclName, "second", SecondTypeName, SecondVarDeclName);
+
+  // X x;
+  // Y y;
+  // std::tie(x, y) = ...;
+  Finder->addMatcher(
+      exprWithCleanups(
+          unless(isInMarco()),
+          has(cxxOperatorCallExpr(
+                  hasOverloadedOperatorName("="),
+                  hasLHS(ignoringImplicit(
+                      callExpr(
+                          callee(
+                              functionDecl(isInStdNamespace(), hasName("tie"))),
+                          hasArgument(
+                              0,
+                              declRefExpr(to(
+                                  varDecl(
+                                      unless(hasAnySpecifiersShouldBeIgnored()),
+                                      unless(isInMarco()))
+                                      .bind(FirstVarDeclName)))),
+                          hasArgument(
+                              1,
+                              declRefExpr(to(
+                                  varDecl(
+                                      unless(hasAnySpecifiersShouldBeIgnored()),
+                                      unless(isInMarco()))
+                                      .bind(SecondVarDeclName)))))
+                          .bind(StdTieExprName))),
+                  hasRHS(expr(hasType(PairType))))
+                  .bind(StdTieAssignStmtName)),
+          hasPreTwoVarDecl(
+              varDecl(equalsBoundNode(std::string(FirstVarDeclName))),
+              varDecl(equalsBoundNode(std::string(SecondVarDeclName))))),
+      this);
+
+  // pair<X, Y> p = ...;
+  // X x = p.first;
+  // Y y = p.second;
+  Finder->addMatcher(
+      declStmt(
+          unless(isInMarco()),
+          hasSingleDecl(
+              varDecl(unless(hasAnySpecifiersShouldBeIgnored()),
+                      hasType(qualType(anyOf(PairType, lValueReferenceType(
+                                                           pointee(PairType))))
+                                  .bind(PairVarTypeName)),
+                      hasInitializer(expr()))
+                  .bind(PairDeclName)),
+          hasNextTwoVarDecl(VarInitWithFirstMember, VarInitWithSecondMember)),
+      this);
+
+  // for (pair<X, Y> p : map) {
+  //    X x = p.first;
+  //    Y y = p.second;
+  // }
+  Finder->addMatcher(
+      cxxForRangeStmt(
+          unless(isInMarco()),
+          hasLoopVariable(
+              varDecl(hasType(qualType(anyOf(PairType, lValueReferenceType(
+                                                           pointee(PairType))))
+                                  .bind(PairVarTypeName)),
+                      hasInitializer(expr()))
+                  .bind(PairDeclName)),
+          hasBody(compoundStmt(hasFirstTwoVarDecl(VarInitWithFirstMember,
+                                                  VarInitWithSecondMember))
+                      .bind(ScopeBlockName)))
+          .bind(ForRangeStmtName),
+      this);
+}
+
+static std::optional<TransferType> getTransferType(const ASTContext &Ctx,
+                                                   QualType ResultType,
+                                                   QualType OriginType) {
+  ResultType = ResultType.getCanonicalType();
+  OriginType = OriginType.getCanonicalType();
+
+  if (ResultType == Ctx.getLValueReferenceType(OriginType.withConst()))
+    return TT_ByConstRef;
+
+  if (ResultType == Ctx.getLValueReferenceType(OriginType))
+    return TT_ByRef;
+
+  if (ResultType == OriginType.withConst())
+    return TT_ByConstVal;
+
+  if (ResultType == OriginType)
+    return TT_ByVal;
+
+  return std::nullopt;
+}
+
+void UseStructuredBindingCheck::check(const MatchFinder::MatchResult &Result) {
+  const auto *FirstVar = Result.Nodes.getNodeAs<VarDecl>(FirstVarDeclName);
+  const auto *SecondVar = Result.Nodes.getNodeAs<VarDecl>(SecondVarDeclName);
+
+  const auto *DS1 = Result.Nodes.getNodeAs<DeclStmt>(FirstDeclStmtName);
+  const auto *DS2 = Result.Nodes.getNodeAs<DeclStmt>(SecondDeclStmtName);
+  const auto *ScopeBlock = Result.Nodes.getNodeAs<CompoundStmt>(ScopeBlockName);
+
+  // Captured structured bindings are a C++20 extension
+  if (!Result.Context->getLangOpts().CPlusPlus20) {
+    if (auto Matchers = match(
+            compoundStmt(
+                hasDescendant(lambdaExpr(hasAnyCapture(capturesVar(varDecl(
+                    anyOf(equalsNode(FirstVar), equalsNode(SecondVar)))))))),
+            *ScopeBlock, *Result.Context);
+        !Matchers.empty())
+      return;
+  }
+
+  const auto *CFRS = Result.Nodes.getNodeAs<CXXForRangeStmt>(ForRangeStmtName);
+  auto DiagAndFix = [&](SourceLocation DiagLoc, SourceRange ReplaceRange,
+                        TransferType TT = TT_ByVal) {
+    StringRef Prefix;
+    switch (TT) {
+    case TT_ByVal:
+      Prefix = "auto";
+      break;
+    case TT_ByConstVal:
+      Prefix = "const auto";
+      break;
+    case TT_ByRef:
+      Prefix = "auto&";
+      break;
+    case TT_ByConstRef:
+      Prefix = "const auto&";
+      break;
+    }
+    std::vector<FixItHint> Hints;
+    if (DS1)
+      Hints.emplace_back(FixItHint::CreateRemoval(DS1->getSourceRange()));
+    if (DS2)
+      Hints.emplace_back(FixItHint::CreateRemoval(DS2->getSourceRange()));
+
+    std::string ReplacementText = Prefix.str() + " [" +
+                                  FirstVar->getNameAsString() + ", " +
+                                  SecondVar->getNameAsString() + "]";
+    if (CFRS)
+      ReplacementText += " :";
+    diag(DiagLoc, "Should use structured binding to decompose pair")
+        << FixItHint::CreateReplacement(ReplaceRange, ReplacementText) << Hints;
+  };
+
+  if (const auto *COCE =
+          Result.Nodes.getNodeAs<CXXOperatorCallExpr>(StdTieAssignStmtName)) {
+    DiagAndFix(COCE->getBeginLoc(),
+               Result.Nodes.getNodeAs<Expr>(StdTieExprName)->getSourceRange());
+    return;
+  }
+
+  // Check whether PairVar, FirstVar and SecondVar have the same transfer type,
+  // so they can be combined to structured binding.
+  const auto *PairVar = Result.Nodes.getNodeAs<VarDecl>(PairDeclName);
+  const Expr *InitE = PairVar->getInit();
+  if (auto Res =
+          match(expr(ignoringCopyCtorAndImplicitCast(expr().bind("init_expr"))),
+                *InitE, *Result.Context);
+      !Res.empty())
+    InitE = Res[0].getNodeAs<Expr>("init_expr");
+
+  std::optional<TransferType> PairCaptureType =
+      getTransferType(*Result.Context, PairVar->getType(), InitE->getType());
+  std::optional<TransferType> FirstVarCaptureType =
+      getTransferType(*Result.Context, FirstVar->getType(),
+                      *Result.Nodes.getNodeAs<QualType>(FirstTypeName));
+  std::optional<TransferType> SecondVarCaptureType =
+      getTransferType(*Result.Context, SecondVar->getType(),
+                      *Result.Nodes.getNodeAs<QualType>(SecondTypeName));
+  if (!PairCaptureType || !FirstVarCaptureType || !SecondVarCaptureType ||
+      *PairCaptureType != *FirstVarCaptureType ||
+      *FirstVarCaptureType != *SecondVarCaptureType)
+    return;
+
+  // Check PairVar is not used except for assignment members to firstVar and
+  // SecondVar.
+  if (auto AllRef = utils::decl_ref_expr::allDeclRefExprs(*PairVar, *ScopeBlock,
+                                                          *Result.Context);
+      AllRef.size() != 2)
+    return;
+
+  DiagAndFix(PairVar->getBeginLoc(),
+             CFRS ? PairVar->getSourceRange()
+                  : SourceRange(PairVar->getBeginLoc(),
+                                Lexer::getLocForEndOfToken(
+                                    PairVar->getLocation(), 0,
+                                    Result.Context->getSourceManager(),
+                                    Result.Context->getLangOpts())),
+             *PairCaptureType);
+}
+
+} // namespace clang::tidy::modernize
diff --git a/clang-tools-extra/clang-tidy/modernize/UseStructuredBindingCheck.h b/clang-tools-extra/clang-tidy/modernize/UseStructuredBindingCheck.h
new file mode 100644
index 0000000000000..63bc0a8c3da45
--- /dev/null
+++ b/clang-tools-extra/clang-tidy/modernize/UseStructuredBindingCheck.h
@@ -0,0 +1,36 @@
+//===--- UseStructuredBindingCheck.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_MODERNIZE_USESTRUCTUREDBINDINGCHECK_H
+#define LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_MODERNIZE_USESTRUCTUREDBINDINGCHECK_H
+
+#include "../ClangTidyCheck.h"
+
+namespace clang::tidy::modernize {
+
+/// Finds places where structured bindings could be used to decompose pairs and
+/// suggests replacing them.
+///
+/// For the user-facing documentation see:
+/// http://clang.llvm.org/extra/clang-tidy/checks/modernize/use-structured-binding.html
+class UseStructuredBindingCheck : public ClangTidyCheck {
+public:
+  UseStructuredBindingCheck(StringRef Name, ClangTidyContext *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.CPlusPlus17;
+  }
+
+private:
+  const std::vector<StringRef> PairTypes;
+};
+
+} // namespace clang::tidy::modernize
+
+#endif // LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_MODERNIZE_USESTRUCTUREDBINDINGCHECK_H
diff --git a/clang-tools-extra/docs/ReleaseNotes.rst b/clang-tools-extra...
[truncated]

@flovent
Copy link
Contributor Author

flovent commented Sep 14, 2025

It's tested in llvm codebase with -DLLVM_ENABLE_PROJECTS="bolt;clang;clang-tools-extra;compiler-rt", since plenty of llvm code it's written in pre-c++17 standard.

It provides ~431 warnings and fixits with check option modernize-use-structured-binding.PairTypes: 'std::pair; llvm::detail::DenseMapPair'

See apply modernize-structured-binding commit based on trunk ad9d551

Copy link
Contributor

@vbvictor vbvictor left a comment

Choose a reason for hiding this comment

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

Very interesting check, thank you for working on it!
For now, looked only at tests/docs


/// Matches a Stmt whose parent is a CompoundStmt, and which is directly
/// following two VarDecls matching the inner matcher.
AST_MATCHER_P(Stmt, hasPreTwoVarDecl,
Copy link
Contributor

@vbvictor vbvictor Sep 21, 2025

Choose a reason for hiding this comment

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

I see that we generally check for CompoundStmt 2 times: one in this matcher, other in registerMatchers with compoundStmt. I think we can restructure this a bit to check of CompoundStmt only one time:

First make Finder->addMatcher(compoundStmt(declStmt(...), ...).bind(stmt)) this will avoid hasParent in matchers which will make everything faster.

Then, in hasPreTwoVarDecl pass ID (stmt) of matched compoundStmt and retrieve it with Nodes.getNodeAs<CompoundStmt>(ID) instead of doing Finder->getASTContext().getParents(Node). You can see this trick in modernize\UseStartsEndsWithCheck.cpp with NotLengthExprForStringNode class.

It may not be a trivial rewrite, so I don't enforce it to do right away, but first part seem not very hard and Finder->addMatcher would look more natural and faster

Copy link
Contributor Author

Choose a reason for hiding this comment

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

First make Finder->addMatcher(compoundStmt(declStmt(...), ...).bind(stmt)) this will avoid hasParent in matchers which will make everything faster.

Doesn't hasDescendant needed here? It will match all child nodes of CompoundStmt.

Copy link
Contributor

Choose a reason for hiding this comment

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

You can use has to match only nodes are direct children of CompoundStmt. declStmt(hasParent(compoundStmt())) should have the same semantics as compoundStmt(has(declStmt())), but thinking about it again, maybe it would not match if there are multiple decompositions:

 {
    auto P = getPair<int, int>(); // match
    int x = P.first;
    int y = P.second;
    
    // maybe match with `hasParent` but NOT with `has(declStmt())`
    auto another_p = getPair<int, int>();
    int another_x = another_p.first;
    int another_y = another_p.second;
 }

Could you add this test?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added in 6d44ea0

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I tried to get ScopeBlock directly in hasPreTwoVarDecl after it's binded by current hasParent(compoundStmt, but it may need to scan all the bindings node by calling Builder::visitMatches.

Copy link
Contributor

Choose a reason for hiding this comment

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

6d44ea0

I'd remove my comments // match and // maybe match with hasParent but NOT with has(declStmt()). I just placed them for current discussion.

Copy link
Contributor

@vbvictor vbvictor Sep 21, 2025

Choose a reason for hiding this comment

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

I tried to get ScopeBlock directly in hasPreTwoVarDecl after it's binded by current hasParent(compoundStmt, but it may need to scan all the bindings node by calling Builder::visitMatches.

Maybe I don't see bigger picture here, but it should only be one call to Builder->removeBindings and then Nodes.getNode(this->BindingID) inside callback, like in "cppcoreguidelines/MissingStdForwardCheck.cpp" hasSameNameAsBoundNode.

Since we don't have any established benchmarks for checks anyway, I'd happy to accept current working matchers.
I personally like "Make It Work Make It Right Make It Fast" strategy, and if we'd be lacking performance in the future we could try rewriting matchers different way.

@flovent
Copy link
Contributor Author

flovent commented Sep 22, 2025

I just find out we should do some special handing for direct inited pair type, like:

{
  std::pair<int, int> P{1, 1};
  int x = P.first;
  int y = P.second; 
}
  
{
  std::pair<int, int> P(1, 1);
  int x = P.first;
  int y = P.second;
}

{
  std::pair<int, int> P;
  int x = P.first;
  int y = P.second;
}

We will give a warning and fix-it, but obviously it will break compilation because we don't have pair type. Any ideas what we should do about them?

  1. Just ignore them
  2. Just warning and don't provide fix-it

This pattern doesn't seems common in real world and the pair should not be created in the first place, so i prefer to ignore them and avoid further matching work.

@flovent
Copy link
Contributor Author

flovent commented Sep 23, 2025

CI failure seems irrelevant.

@flovent
Copy link
Contributor Author

flovent commented Sep 28, 2025

ping.

And any opinions for #158462 (comment)?

@vbvictor
Copy link
Contributor

vbvictor commented Sep 29, 2025

And any opinions for #158462 (comment)?

I'd go for what's easier for now (simply ignoring). AFAIK, this case simply don't match right now? Could you add these examples in tests

P.S. I hope to do another round of review in upcoming days

@flovent
Copy link
Contributor Author

flovent commented Sep 29, 2025

I'd go for what's easier for now (simply ignoring). AFAIK, this case simply don't match right now? Could you add these examples in tests

Sadly it matches, but it can be ignored without much work, just need to check VarDecl's init style.

P.S. I hope to do another round of review in upcoming days

Thanks!

@@ -0,0 +1,622 @@
// RUN: %check_clang_tidy -check-suffix=ALL,CPP20ORLATER -std=c++20-or-later %s modernize-use-structured-binding %t -- -- -I %S/Inputs/use-structured-binding/
// RUN: %check_clang_tidy -check-suffix=ALL -std=c++17 %s modernize-use-structured-binding %t -- -- -I %S/Inputs/use-structured-binding/
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: IIRC you can omit "ALL" suffix here and in first RUN write -check-suffix=,CPP20ORLATER

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Wonderful! ALL suffix is learned from other testfiles, that saves many times and makes it look simpler.
fbbbdd5

Comment on lines +21 to +22
int x = P.first;
int y = P.second; // REMOVE
Copy link
Contributor

Choose a reason for hiding this comment

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

Don't we need to write // REMOVE1 and // REMOVE2 to check that both lines are deleted?
Maybe we should use https://llvm.org/docs/CommandGuide/FileCheck.html#the-check-next-directive

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added in 8d3054c

Because of refatoring job of matchTwoVarDecl, right now we remove continuous DeclStmts, so i only check for // REMOVE in next line here.

TestClass& operator++();
TestClass(int x, int y) : a(x), b(y) {}
};

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 add a test when we decompose pair with operator*(), e.g.:

std::unordered_map::iterator it;
auto [key, value] = *it;

Good to also check iteration over containers (with involving iterators):

for (auto [k, v] : my_map);

It's a popular pattern IMO.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I add testcase for for (auto [k, v] : my_map); in 4da01f9,
For the iterator case:

std::unordered_map::iterator it;
auto key = it->first;
auto value = it->second;

-> should be operator-> too? it's not supported because we check MemberExpr to VarDecl now and it might has side effects for operator->(We don't know whether it just get it's member).

AST_MATCHER(CXXRecordDecl, isPairType) {
return llvm::all_of(Node.fields(), [](const FieldDecl *FD) {
return FD->getAccess() == AS_public &&
(FD->getName() == "first" || FD->getName() == "second");
Copy link
Contributor

Choose a reason for hiding this comment

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

I think we can allow fields to have different names as long as there are two of them?

Copy link
Contributor Author

@flovent flovent Oct 3, 2025

Choose a reason for hiding this comment

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

We need those members' name or FieldDeclbindings for further matching, so it's needed to setBinding for them.

It might be much work, i am planning to expand this after this PR is finished.

return Node.getBeginLoc().isMacroID() || Node.getEndLoc().isMacroID();
}

AST_MATCHER_P(Expr, ignoringCopyCtorAndImplicitCast,
Copy link
Contributor

Choose a reason for hiding this comment

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

So we essentially do InnerMatcher(CtorE->getArg(0)), could we make it into classname?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

what does classname meaning here?

Copy link
Contributor

Choose a reason for hiding this comment

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

I meant name of the matcher.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I am still confused, changing current name of matcher? how to make it into name.

return varDecl(
ExtraMatcher,
hasInitializer(
ignoringImpCasts(ignoringCopyCtorAndImplicitCast(memberExpr(
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 need one more ignoringImpCasts here if ignoringCopyCtorAndImplicitCast already handles it?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants