[clang] [Clang] Fix synthesis of defaulted operator==/<=> when class has an anonymous struct member (PR #93380)

2024-05-25 Thread via cfe-commits

llvmbot wrote:




@llvm/pr-subscribers-clang

Author: Mital Ashok (MitalAshok)


Changes

Fixes #92497

The existing implementation did try to drill into anonymous structs and compare 
them member by member, but it did not properly build up the member access 
expressions to include the anonymous struct members.

Note that this is different behaviour from GCC's anonymous struct extension 
where the defaulted comparison would compare `LHS.anonymous struct` 
with `RHS.anonymous struct`, which would fail if there is no overload 
for those types.

Example of the difference:

```c++
struct X {
struct {
bool b;
};
bool c;
templatetypename T
friend constexpr bool operator==(T L, T R) {
// With GCC, T is the type of the anonymous struct
// With Clang, this operator isn't used
// (L.b and R.b are compared directly in the below operator==)
static_assert(sizeof(T) == sizeof(bool));
return L.b == R.b;
}
friend constexpr bool operator==(const X L, const X R) = default;
};

static_assert(X{} == X{});
```

This appears to be consistent with the Microsoft extension for anonymous structs

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


3 Files Affected:

- (modified) clang/docs/ReleaseNotes.rst (+2) 
- (modified) clang/lib/Sema/SemaDeclCXX.cpp (+57-6) 
- (modified) clang/test/SemaCXX/anonymous-struct.cpp (+78-1) 


``diff
diff --git a/clang/docs/ReleaseNotes.rst b/clang/docs/ReleaseNotes.rst
index d023f53754cb3..7985ab35439d2 100644
--- a/clang/docs/ReleaseNotes.rst
+++ b/clang/docs/ReleaseNotes.rst
@@ -794,6 +794,8 @@ Bug Fixes to C++ Support
   Fixes (#GH87210), (GH89541).
 - Clang no longer tries to check if an expression is immediate-escalating in 
an unevaluated context.
   Fixes (#GH91308).
+- Fix defaulted ``operator==``/``operator<=>`` not working properly with
+  classes that have anonymous struct members (#GH92497).
 
 Bug Fixes to AST Handling
 ^
diff --git a/clang/lib/Sema/SemaDeclCXX.cpp b/clang/lib/Sema/SemaDeclCXX.cpp
index 8ab429e2a136e..f573012ceacb9 100644
--- a/clang/lib/Sema/SemaDeclCXX.cpp
+++ b/clang/lib/Sema/SemaDeclCXX.cpp
@@ -7948,7 +7948,13 @@ class DefaultedComparisonVisitor {
 
 case DefaultedComparisonKind::Equal:
 case DefaultedComparisonKind::ThreeWay:
+  assert(
+  AnonymousStructs.empty() &&
+  "Anonymous structs stack should be empty before visiting 
subobjects");
   getDerived().visitSubobjects(Results, RD, ParamLvalType.getQualifiers());
+  assert(AnonymousStructs.empty() &&
+ "Anonymous structs stack should become empty again after visiting 
"
+ "subobjects");
   return Results;
 
 case DefaultedComparisonKind::NotEqual:
@@ -7985,6 +7991,7 @@ class DefaultedComparisonVisitor {
 continue;
   // Recursively expand anonymous structs.
   if (Field->isAnonymousStructOrUnion()) {
+AnonymousStructContextRAII R(*this, Field);
 if (visitSubobjects(Results, Field->getType()->getAsCXXRecordDecl(),
 Quals))
   return true;
@@ -8027,6 +8034,30 @@ class DefaultedComparisonVisitor {
   FunctionDecl *FD;
   DefaultedComparisonKind DCK;
   UnresolvedSet<16> Fns;
+  std::vector AnonymousStructs;
+
+private:
+  struct AnonymousStructContextRAII {
+DefaultedComparisonVisitor 
+#ifndef NDEBUG
+FieldDecl *FD;
+#endif
+AnonymousStructContextRAII(AnonymousStructContextRAII &&) = delete;
+explicit AnonymousStructContextRAII(DefaultedComparisonVisitor ,
+FieldDecl *FD)
+: Self(Self) {
+#ifndef NDEBUG
+  this->FD = FD;
+#endif
+  Self.AnonymousStructs.push_back(FD);
+}
+~AnonymousStructContextRAII() {
+  assert(!Self.AnonymousStructs.empty() &&
+ Self.AnonymousStructs.back() == FD &&
+ "Invalid stack of anonymous structs");
+  Self.AnonymousStructs.pop_back();
+}
+  };
 };
 
 /// Information about a defaulted comparison, as determined by
@@ -8535,6 +8566,8 @@ class DefaultedComparisonSynthesizer
   }
 
   ExprPair getBase(CXXBaseSpecifier *Base) {
+assert(AnonymousStructs.empty() &&
+   "Visiting base class while inside an anonymous struct?");
 ExprPair Obj = getCompleteObject();
 if (Obj.first.isInvalid() || Obj.second.isInvalid())
   return {ExprError(), ExprError()};
@@ -8550,12 +8583,30 @@ class DefaultedComparisonSynthesizer
 if (Obj.first.isInvalid() || Obj.second.isInvalid())
   return {ExprError(), ExprError()};
 
-DeclAccessPair Found = DeclAccessPair::make(Field, Field->getAccess());
-DeclarationNameInfo NameInfo(Field->getDeclName(), Loc);
-return {S.BuildFieldReferenceExpr(Obj.first.get(), /*IsArrow=*/false, Loc,
-  CXXScopeSpec(), Field, Found, NameInfo),
-S.BuildFieldReferenceExpr(Obj.second.get(), /*IsArrow=*/false, Loc,
-  

[clang] [Clang] Fix synthesis of defaulted operator==/<=> when class has an anonymous struct member (PR #93380)

2024-05-25 Thread Mital Ashok via cfe-commits

https://github.com/MitalAshok created 
https://github.com/llvm/llvm-project/pull/93380

Fixes #92497

The existing implementation did try to drill into anonymous structs and compare 
them member by member, but it did not properly build up the member access 
expressions to include the anonymous struct members.

Note that this is different behaviour from GCC's anonymous struct extension 
where the defaulted comparison would compare `LHS.` with 
`RHS.`, which would fail if there is no overload for those 
types.

Example of the difference:

```c++
struct X {
struct {
bool b;
};
bool c;
template
friend constexpr bool operator==(T& L, T& R) {
// With GCC, T is the type of the anonymous struct
// With Clang, this operator isn't used
// (L.b and R.b are compared directly in the below operator==)
static_assert(sizeof(T) == sizeof(bool));
return L.b == R.b;
}
friend constexpr bool operator==(const X& L, const X& R) = default;
};

static_assert(X{} == X{});
```

This appears to be consistent with the Microsoft extension for anonymous structs

>From afb148f97eee38cdaa867cad4138bf7d7a6f6132 Mon Sep 17 00:00:00 2001
From: Mital Ashok 
Date: Sat, 25 May 2024 15:49:10 +0100
Subject: [PATCH] [Clang] Fix synthesis of defaulted operator==/<=> when class
 has an anonymous struct member

---
 clang/docs/ReleaseNotes.rst |  2 +
 clang/lib/Sema/SemaDeclCXX.cpp  | 63 ++--
 clang/test/SemaCXX/anonymous-struct.cpp | 79 -
 3 files changed, 137 insertions(+), 7 deletions(-)

diff --git a/clang/docs/ReleaseNotes.rst b/clang/docs/ReleaseNotes.rst
index d023f53754cb3..7985ab35439d2 100644
--- a/clang/docs/ReleaseNotes.rst
+++ b/clang/docs/ReleaseNotes.rst
@@ -794,6 +794,8 @@ Bug Fixes to C++ Support
   Fixes (#GH87210), (GH89541).
 - Clang no longer tries to check if an expression is immediate-escalating in 
an unevaluated context.
   Fixes (#GH91308).
+- Fix defaulted ``operator==``/``operator<=>`` not working properly with
+  classes that have anonymous struct members (#GH92497).
 
 Bug Fixes to AST Handling
 ^
diff --git a/clang/lib/Sema/SemaDeclCXX.cpp b/clang/lib/Sema/SemaDeclCXX.cpp
index 8ab429e2a136e..f573012ceacb9 100644
--- a/clang/lib/Sema/SemaDeclCXX.cpp
+++ b/clang/lib/Sema/SemaDeclCXX.cpp
@@ -7948,7 +7948,13 @@ class DefaultedComparisonVisitor {
 
 case DefaultedComparisonKind::Equal:
 case DefaultedComparisonKind::ThreeWay:
+  assert(
+  AnonymousStructs.empty() &&
+  "Anonymous structs stack should be empty before visiting 
subobjects");
   getDerived().visitSubobjects(Results, RD, ParamLvalType.getQualifiers());
+  assert(AnonymousStructs.empty() &&
+ "Anonymous structs stack should become empty again after visiting 
"
+ "subobjects");
   return Results;
 
 case DefaultedComparisonKind::NotEqual:
@@ -7985,6 +7991,7 @@ class DefaultedComparisonVisitor {
 continue;
   // Recursively expand anonymous structs.
   if (Field->isAnonymousStructOrUnion()) {
+AnonymousStructContextRAII R(*this, Field);
 if (visitSubobjects(Results, Field->getType()->getAsCXXRecordDecl(),
 Quals))
   return true;
@@ -8027,6 +8034,30 @@ class DefaultedComparisonVisitor {
   FunctionDecl *FD;
   DefaultedComparisonKind DCK;
   UnresolvedSet<16> Fns;
+  std::vector AnonymousStructs;
+
+private:
+  struct AnonymousStructContextRAII {
+DefaultedComparisonVisitor 
+#ifndef NDEBUG
+FieldDecl *FD;
+#endif
+AnonymousStructContextRAII(AnonymousStructContextRAII &&) = delete;
+explicit AnonymousStructContextRAII(DefaultedComparisonVisitor ,
+FieldDecl *FD)
+: Self(Self) {
+#ifndef NDEBUG
+  this->FD = FD;
+#endif
+  Self.AnonymousStructs.push_back(FD);
+}
+~AnonymousStructContextRAII() {
+  assert(!Self.AnonymousStructs.empty() &&
+ Self.AnonymousStructs.back() == FD &&
+ "Invalid stack of anonymous structs");
+  Self.AnonymousStructs.pop_back();
+}
+  };
 };
 
 /// Information about a defaulted comparison, as determined by
@@ -8535,6 +8566,8 @@ class DefaultedComparisonSynthesizer
   }
 
   ExprPair getBase(CXXBaseSpecifier *Base) {
+assert(AnonymousStructs.empty() &&
+   "Visiting base class while inside an anonymous struct?");
 ExprPair Obj = getCompleteObject();
 if (Obj.first.isInvalid() || Obj.second.isInvalid())
   return {ExprError(), ExprError()};
@@ -8550,12 +8583,30 @@ class DefaultedComparisonSynthesizer
 if (Obj.first.isInvalid() || Obj.second.isInvalid())
   return {ExprError(), ExprError()};
 
-DeclAccessPair Found = DeclAccessPair::make(Field, Field->getAccess());
-DeclarationNameInfo NameInfo(Field->getDeclName(), Loc);
-return {S.BuildFieldReferenceExpr(Obj.first.get(), /*IsArrow=*/false,