llvmbot wrote:

<!--LLVM PR SUMMARY COMMENT-->

@llvm/pr-subscribers-clang-tidy

Author: Helmut Januschka (hjanuschka)

<details>
<summary>Changes</summary>

Add new clang-tidy check that finds classes which could be aggregates if their 
trivial forwarding constructors were removed.

A constructor is considered a trivial forwarder when it takes one parameter per 
non-static data member, initializes each member from the corresponding 
parameter in declaration order, and has an empty body. Removing such 
constructors enables aggregate initialization and, in C++20, designated 
initializers.

```cpp
// Before
struct Point {
  int X;
  int Y;
  Point(int X, int Y) : X(X), Y(Y) {}
};
Point p(1, 2);

// After -- remove the constructor
struct Point {
  int X;
  int Y;
};
Point p{1, 2};            // aggregate initialization
Point q{.X = 1, .Y = 2}; // designated initializers (C++20)
```

The check will **not** flag a class if it:
- has virtual functions
- has private or protected non-static data members
- has virtual, private, or protected base classes
- has base classes (before C++17)
- has a user-provided destructor
- has additional non-trivial constructors
- is a template specialization

---
Full diff: https://github.com/llvm/llvm-project/pull/182061.diff


8 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/UseAggregateCheck.cpp (+215) 
- (added) clang-tools-extra/clang-tidy/modernize/UseAggregateCheck.h (+39) 
- (modified) clang-tools-extra/docs/ReleaseNotes.rst (+7) 
- (modified) clang-tools-extra/docs/clang-tidy/checks/list.rst (+1) 
- (added) clang-tools-extra/docs/clang-tidy/checks/modernize/use-aggregate.rst 
(+43) 
- (added) 
clang-tools-extra/test/clang-tidy/checkers/modernize/use-aggregate.cpp (+145) 


``````````diff
diff --git a/clang-tools-extra/clang-tidy/modernize/CMakeLists.txt 
b/clang-tools-extra/clang-tidy/modernize/CMakeLists.txt
index cc4cc7a02b594..92aa7b9388cb2 100644
--- a/clang-tools-extra/clang-tidy/modernize/CMakeLists.txt
+++ b/clang-tools-extra/clang-tidy/modernize/CMakeLists.txt
@@ -31,6 +31,7 @@ add_clang_library(clangTidyModernizeModule STATIC
   ShrinkToFitCheck.cpp
   TypeTraitsCheck.cpp
   UnaryStaticAssertCheck.cpp
+  UseAggregateCheck.cpp
   UseAutoCheck.cpp
   UseBoolLiteralsCheck.cpp
   UseConstraintsCheck.cpp
diff --git a/clang-tools-extra/clang-tidy/modernize/ModernizeTidyModule.cpp 
b/clang-tools-extra/clang-tidy/modernize/ModernizeTidyModule.cpp
index fcb860d8c5298..9c6758cf99ecd 100644
--- a/clang-tools-extra/clang-tidy/modernize/ModernizeTidyModule.cpp
+++ b/clang-tools-extra/clang-tidy/modernize/ModernizeTidyModule.cpp
@@ -34,6 +34,7 @@
 #include "UseAutoCheck.h"
 #include "UseBoolLiteralsCheck.h"
 #include "UseConstraintsCheck.h"
+#include "UseAggregateCheck.h"
 #include "UseDefaultMemberInitCheck.h"
 #include "UseDesignatedInitializersCheck.h"
 #include "UseEmplaceCheck.h"
@@ -116,6 +117,8 @@ class ModernizeModule : public ClangTidyModule {
     CheckFactories.registerCheck<TypeTraitsCheck>("modernize-type-traits");
     CheckFactories.registerCheck<UnaryStaticAssertCheck>(
         "modernize-unary-static-assert");
+    CheckFactories.registerCheck<UseAggregateCheck>(
+        "modernize-use-aggregate");
     CheckFactories.registerCheck<UseAutoCheck>("modernize-use-auto");
     CheckFactories.registerCheck<UseBoolLiteralsCheck>(
         "modernize-use-bool-literals");
diff --git a/clang-tools-extra/clang-tidy/modernize/UseAggregateCheck.cpp 
b/clang-tools-extra/clang-tidy/modernize/UseAggregateCheck.cpp
new file mode 100644
index 0000000000000..a1c128fe53483
--- /dev/null
+++ b/clang-tools-extra/clang-tidy/modernize/UseAggregateCheck.cpp
@@ -0,0 +1,215 @@
+//===----------------------------------------------------------------------===//
+//
+// 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 "UseAggregateCheck.h"
+#include "clang/AST/ASTContext.h"
+#include "clang/AST/DeclCXX.h"
+#include "clang/ASTMatchers/ASTMatchFinder.h"
+#include "clang/Lex/Lexer.h"
+
+using namespace clang::ast_matchers;
+
+namespace clang::tidy::modernize {
+
+/// Check whether \p Ctor is a trivial forwarding constructor: each parameter
+/// is used to initialise the corresponding member (in declaration order) and
+/// the body is empty.
+static bool isTrivialForwardingConstructor(const CXXConstructorDecl *Ctor) {
+  if (!Ctor || !Ctor->hasBody())
+    return false;
+
+  // Body must be an empty compound statement.
+  const auto *Body = dyn_cast<CompoundStmt>(Ctor->getBody());
+  if (!Body || !Body->body_empty())
+    return false;
+
+  const CXXRecordDecl *Record = Ctor->getParent();
+
+  // Collect non-static data members in declaration order.
+  SmallVector<const FieldDecl *, 8> Fields;
+  for (const auto *Field : Record->fields())
+    Fields.push_back(Field);
+
+  // Number of parameters must match number of fields.
+  if (Ctor->getNumParams() != Fields.size())
+    return false;
+
+  // Number of member initializers must match number of fields (no base
+  // class inits, no extra inits).
+  unsigned NumMemberInits = 0;
+  for (const auto *Init : Ctor->inits()) {
+    if (Init->isMemberInitializer())
+      ++NumMemberInits;
+    else
+      return false; // base class or delegating init -- bail out
+  }
+  if (NumMemberInits != Fields.size())
+    return false;
+
+  // Walk initializers and check each one initializes the matching field
+  // from the matching parameter.
+  unsigned FieldIdx = 0;
+  for (const auto *Init : Ctor->inits()) {
+    if (!Init->isMemberInitializer())
+      return false;
+
+    // Must match the field at the current position.
+    if (Init->getMember() != Fields[FieldIdx])
+      return false;
+
+    const Expr *InitExpr = Init->getInit()->IgnoreImplicit();
+
+    // Handle CXXConstructExpr wrapping the parameter (for class types).
+    if (const auto *Construct = dyn_cast<CXXConstructExpr>(InitExpr)) {
+      if (Construct->getNumArgs() != 1)
+        return false;
+      // Must be a copy or move constructor call.
+      const CXXConstructorDecl *InitCtor = Construct->getConstructor();
+      if (!InitCtor->isCopyOrMoveConstructor())
+        return false;
+      InitExpr = Construct->getArg(0)->IgnoreImplicit();
+    }
+
+    // The init expression must be a DeclRefExpr to the corresponding param.
+    const auto *DRE = dyn_cast<DeclRefExpr>(InitExpr);
+    if (!DRE)
+      return false;
+    const auto *PVD = dyn_cast<ParmVarDecl>(DRE->getDecl());
+    if (!PVD || PVD != Ctor->getParamDecl(FieldIdx))
+      return false;
+
+    ++FieldIdx;
+  }
+
+  return true;
+}
+
+/// Check whether the class would be a valid aggregate if all user-declared
+/// constructors were removed.
+static bool canBeAggregate(const CXXRecordDecl *Record,
+                           const LangOptions &LangOpts) {
+  if (!Record || !Record->hasDefinition())
+    return false;
+
+  // Must not have virtual functions.
+  if (Record->isPolymorphic())
+    return false;
+
+  // Must not have private or protected non-static data members.
+  for (const auto *Field : Record->fields())
+    if (Field->getAccess() != AS_public)
+      return false;
+
+  // Must not have virtual, private, or protected base classes.
+  for (const auto &Base : Record->bases()) {
+    if (Base.isVirtual())
+      return false;
+    if (Base.getAccessSpecifier() != AS_public)
+      return false;
+  }
+
+  // C++17 and later allow non-virtual public base classes in aggregates.
+  // Before C++17, aggregates cannot have base classes at all.
+  if (!LangOpts.CPlusPlus17 && Record->getNumBases() > 0)
+    return false;
+
+  return true;
+}
+
+void UseAggregateCheck::registerMatchers(MatchFinder *Finder) {
+  // Match class/struct definitions that have at least one user-provided
+  // constructor.
+  Finder->addMatcher(
+      cxxRecordDecl(
+          isDefinition(),
+          unless(isImplicit()),
+          unless(isLambda()),
+          has(cxxConstructorDecl(isUserProvided(), unless(isDeleted()))))
+          .bind("record"),
+      this);
+}
+
+void UseAggregateCheck::check(const MatchFinder::MatchResult &Result) {
+  const auto *Record = Result.Nodes.getNodeAs<CXXRecordDecl>("record");
+  if (!Record)
+    return;
+
+  // Skip records in system headers.
+  if (Record->getLocation().isInvalid() ||
+      Result.SourceManager->isInSystemHeader(Record->getLocation()))
+    return;
+
+  // Skip template specializations to avoid false positives.
+  if (isa<ClassTemplateSpecializationDecl>(Record))
+    return;
+
+  // Skip if no fields (empty structs are already aggregates by default).
+  if (Record->field_empty())
+    return;
+
+  // Check aggregate preconditions (ignoring constructors).
+  if (!canBeAggregate(Record, getLangOpts()))
+    return;
+
+  // Collect all user-declared constructors.
+  SmallVector<const CXXConstructorDecl *, 4> UserCtors;
+  for (const auto *Decl : Record->decls()) {
+    const auto *Ctor = dyn_cast<CXXConstructorDecl>(Decl);
+    if (!Ctor || Ctor->isImplicit())
+      continue;
+
+    // If there is any user-declared constructor that is not a trivial
+    // forwarding constructor and not defaulted/deleted, bail out. We cannot
+    // safely suggest removing it.
+    if (Ctor->isDeleted() || Ctor->isDefaulted()) {
+      // Explicit default/delete still counts as user-declared in C++20
+      // aggregate rules, but we focus on user-provided constructors.
+      // In C++20 mode, even =default prevents aggregate, so we should
+      // flag those too.
+      if (getLangOpts().CPlusPlus20)
+        UserCtors.push_back(Ctor);
+      continue;
+    }
+
+    if (!isTrivialForwardingConstructor(Ctor))
+      return; // Non-trivial constructor -- not safe to remove
+
+    UserCtors.push_back(Ctor);
+  }
+
+  if (UserCtors.empty())
+    return;
+
+  // Check that there is no user-provided destructor.
+  if (const auto *Dtor = Record->getDestructor())
+    if (Dtor->isUserProvided())
+      return;
+
+  // Find the primary forwarding constructor to diagnose on.
+  const CXXConstructorDecl *PrimaryCtor = nullptr;
+  for (const auto *Ctor : UserCtors) {
+    if (!Ctor->isDeleted() && !Ctor->isDefaulted()) {
+      PrimaryCtor = Ctor;
+      break;
+    }
+  }
+
+  if (!PrimaryCtor)
+    return;
+
+  // Emit diagnostic on the class, with a note on the constructor.
+  diag(Record->getLocation(),
+       "'%0' can be an aggregate type if the forwarding constructor "
+       "is removed")
+      << Record->getName();
+  diag(PrimaryCtor->getLocation(),
+       "remove this constructor to enable aggregate initialization",
+       DiagnosticIDs::Note);
+}
+
+} // namespace clang::tidy::modernize
diff --git a/clang-tools-extra/clang-tidy/modernize/UseAggregateCheck.h 
b/clang-tools-extra/clang-tidy/modernize/UseAggregateCheck.h
new file mode 100644
index 0000000000000..833b654ec808c
--- /dev/null
+++ b/clang-tools-extra/clang-tidy/modernize/UseAggregateCheck.h
@@ -0,0 +1,39 @@
+//===----------------------------------------------------------------------===//
+//
+// 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_USEAGGREGATECHECK_H
+#define LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_MODERNIZE_USEAGGREGATECHECK_H
+
+#include "../ClangTidyCheck.h"
+
+namespace clang::tidy::modernize {
+
+/// Finds classes that could be aggregates if their trivial constructors
+/// were removed.
+///
+/// A constructor is considered trivial when it simply forwards each parameter
+/// to a member in declaration order and has an empty body. Removing such
+/// constructors enables aggregate initialization, which is often clearer and
+/// supports designated initializers (C++20).
+///
+/// For the user-facing documentation see:
+/// https://clang.llvm.org/extra/clang-tidy/checks/modernize/use-aggregate.html
+class UseAggregateCheck : public ClangTidyCheck {
+public:
+  UseAggregateCheck(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.CPlusPlus;
+  }
+};
+
+} // namespace clang::tidy::modernize
+
+#endif // LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_MODERNIZE_USEAGGREGATECHECK_H
diff --git a/clang-tools-extra/docs/ReleaseNotes.rst 
b/clang-tools-extra/docs/ReleaseNotes.rst
index 68bab88146241..7cdeabc1570ee 100644
--- a/clang-tools-extra/docs/ReleaseNotes.rst
+++ b/clang-tools-extra/docs/ReleaseNotes.rst
@@ -121,6 +121,13 @@ New checks
   ``llvm::to_vector(llvm::make_filter_range(...))`` that can be replaced with
   ``llvm::map_to_vector`` and ``llvm::filter_to_vector``.
 
+- New :doc:`modernize-use-aggregate
+  <clang-tidy/checks/modernize/use-aggregate>` check.
+
+  Finds classes that could be aggregates if their trivial forwarding
+  constructors were removed, enabling aggregate initialization and
+  designated initializers.
+
 - New :doc:`modernize-use-string-view
   <clang-tidy/checks/modernize/use-string-view>` 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..1c1d9038cb2c7 100644
--- a/clang-tools-extra/docs/clang-tidy/checks/list.rst
+++ b/clang-tools-extra/docs/clang-tidy/checks/list.rst
@@ -311,6 +311,7 @@ Clang-Tidy Checks
    :doc:`modernize-shrink-to-fit <modernize/shrink-to-fit>`, "Yes"
    :doc:`modernize-type-traits <modernize/type-traits>`, "Yes"
    :doc:`modernize-unary-static-assert <modernize/unary-static-assert>`, "Yes"
+   :doc:`modernize-use-aggregate <modernize/use-aggregate>`,
    :doc:`modernize-use-auto <modernize/use-auto>`, "Yes"
    :doc:`modernize-use-bool-literals <modernize/use-bool-literals>`, "Yes"
    :doc:`modernize-use-constraints <modernize/use-constraints>`, "Yes"
diff --git 
a/clang-tools-extra/docs/clang-tidy/checks/modernize/use-aggregate.rst 
b/clang-tools-extra/docs/clang-tidy/checks/modernize/use-aggregate.rst
new file mode 100644
index 0000000000000..4effe372d9bc1
--- /dev/null
+++ b/clang-tools-extra/docs/clang-tidy/checks/modernize/use-aggregate.rst
@@ -0,0 +1,43 @@
+.. title:: clang-tidy - modernize-use-aggregate
+
+modernize-use-aggregate
+=======================
+
+Finds classes and structs that could be aggregates if their trivial
+forwarding constructors were removed.
+
+A constructor is considered a trivial forwarder when it takes one
+parameter per non-static data member, initializes each member from the
+corresponding parameter in declaration order, and has an empty body.
+Removing such constructors enables aggregate initialization and, in
+C++20, designated initializers.
+
+.. code-block:: c++
+
+  // Before
+  struct Point {
+    int X;
+    int Y;
+    Point(int X, int Y) : X(X), Y(Y) {}
+  };
+
+  Point p(1, 2);
+
+  // After -- remove the constructor
+  struct Point {
+    int X;
+    int Y;
+  };
+
+  Point p{1, 2};          // aggregate initialization
+  Point q{.X = 1, .Y = 2}; // designated initializers (C++20)
+
+The check will not flag a class if it:
+
+- has virtual functions,
+- has private or protected non-static data members,
+- has virtual, private, or protected base classes,
+- has base classes (before C++17),
+- has a user-provided destructor,
+- has additional non-trivial constructors, or
+- is a template specialization.
diff --git 
a/clang-tools-extra/test/clang-tidy/checkers/modernize/use-aggregate.cpp 
b/clang-tools-extra/test/clang-tidy/checkers/modernize/use-aggregate.cpp
new file mode 100644
index 0000000000000..f34366747795d
--- /dev/null
+++ b/clang-tools-extra/test/clang-tidy/checkers/modernize/use-aggregate.cpp
@@ -0,0 +1,145 @@
+// RUN: %check_clang_tidy %s modernize-use-aggregate %t
+
+// Positive: simple forwarding constructor.
+struct Point {
+  // CHECK-MESSAGES: :[[@LINE-1]]:8: warning: 'Point' can be an aggregate type 
if the forwarding constructor is removed [modernize-use-aggregate]
+  int X;
+  int Y;
+  Point(int X, int Y) : X(X), Y(Y) {}
+  // CHECK-MESSAGES: :[[@LINE-1]]:3: note: remove this constructor to enable 
aggregate initialization
+};
+
+// Positive: forwarding constructor with class-type member (copy/move).
+namespace std {
+template <typename T>
+class basic_string {
+public:
+  basic_string();
+  basic_string(const basic_string &);
+  basic_string(basic_string &&);
+  basic_string(const char *);
+};
+using string = basic_string<char>;
+} // namespace std
+
+struct Person {
+  // CHECK-MESSAGES: :[[@LINE-1]]:8: warning: 'Person' can be an aggregate 
type if the forwarding constructor is removed [modernize-use-aggregate]
+  std::string Name;
+  int Age;
+  Person(std::string Name, int Age) : Name(Name), Age(Age) {}
+  // CHECK-MESSAGES: :[[@LINE-1]]:3: note: remove this constructor to enable 
aggregate initialization
+};
+
+// Negative: constructor does more than forward.
+struct WithLogic {
+  int X;
+  int Y;
+  WithLogic(int X, int Y) : X(X), Y(Y + 1) {}
+};
+
+// Negative: constructor body is not empty.
+struct WithBody {
+  int X;
+  int Y;
+  WithBody(int X, int Y) : X(X), Y(Y) { X++; }
+};
+
+// Negative: has virtual functions.
+struct WithVirtual {
+  int X;
+  virtual void foo();
+  WithVirtual(int X) : X(X) {}
+};
+
+// Negative: has private data members.
+class WithPrivate {
+  int X;
+public:
+  WithPrivate(int X) : X(X) {}
+};
+
+// Negative: has protected data members.
+struct WithProtected {
+protected:
+  int X;
+public:
+  WithProtected(int X) : X(X) {}
+};
+
+// Negative: wrong parameter count.
+struct WrongParamCount {
+  int X;
+  int Y;
+  WrongParamCount(int X) : X(X), Y(0) {}
+};
+
+// Negative: wrong init order (param 1 -> field 0, param 0 -> field 1).
+struct WrongOrder {
+  int X;
+  int Y;
+  WrongOrder(int A, int B) : X(B), Y(A) {}
+};
+
+// Negative: has non-trivial destructor.
+struct WithDestructor {
+  int X;
+  WithDestructor(int X) : X(X) {}
+  ~WithDestructor() {}
+};
+
+// Negative: has additional non-trivial constructor.
+struct MultiCtor {
+  int X;
+  MultiCtor(int X) : X(X) {}
+  MultiCtor(double) : X(0) {}
+};
+
+// Negative: empty struct (already an aggregate).
+struct Empty {
+  Empty() {}
+};
+
+// Negative: template specialization.
+template <typename T>
+struct Templated {
+  T X;
+  Templated(T X) : X(X) {}
+};
+Templated<int> TI(1);
+
+// Negative: virtual base class.
+struct Base {};
+struct WithVirtualBase : virtual Base {
+  int X;
+  WithVirtualBase(int X) : X(X) {}
+};
+
+// Negative: private base class.
+struct WithPrivateBase : private Base {
+  int X;
+  WithPrivateBase(int X) : X(X) {}
+};
+
+// Positive: three fields.
+struct Triple {
+  // CHECK-MESSAGES: :[[@LINE-1]]:8: warning: 'Triple' can be an aggregate 
type if the forwarding constructor is removed [modernize-use-aggregate]
+  int A;
+  int B;
+  int C;
+  Triple(int A, int B, int C) : A(A), B(B), C(C) {}
+  // CHECK-MESSAGES: :[[@LINE-1]]:3: note: remove this constructor to enable 
aggregate initialization
+};
+
+// Positive: single field.
+struct Single {
+  // CHECK-MESSAGES: :[[@LINE-1]]:8: warning: 'Single' can be an aggregate 
type if the forwarding constructor is removed [modernize-use-aggregate]
+  int X;
+  Single(int X) : X(X) {}
+  // CHECK-MESSAGES: :[[@LINE-1]]:3: note: remove this constructor to enable 
aggregate initialization
+};
+
+// Negative: has base class initializer.
+struct Derived : Base {
+  int X;
+  Derived(int X) : Base(), X(X) {}
+};

``````````

</details>


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

Reply via email to