[clang] [clang][CodeGen] Fix crash when calling a defaulted comparison operator of a union (PR #231232)

Akash Manna via cfe-commits cfe-commits at lists.llvm.org
Wed Oct 21 22:02:09 PDT 2026


https://github.com/akash-manna-sky updated https://github.com/llvm/llvm-project/pull/231232

>From 91aa8b46b98ecb5ac2d2a04cff6db61c2fe8bd6c Mon Sep 17 00:00:00 2001
From: Akash Manna <akash.manna.mymail at gmail.com>
Date: Sun, 11 Oct 2026 21:08:21 +0530
Subject: [PATCH 1/3] [clang][CodeGen] Fix crash when calling a defaulted
 comparison operator of a union

EmitCXXMemberOrOperatorMemberCallExpr treated every defaulted member of a union as trivial for codegen. That clause exists so that a defaulted union copy or move operation is emitted as a memcpy, but since C++20 a defaulted operator== or operator<=> matches it as well, fits none of the trivial sub-cases and hits the "unknown trivial member function" assert.

Use isMemcpyEquivalentSpecialMember, which covers exactly the copy and move operations the clause was written for, so a defaulted comparison operator is emitted and called as an ordinary member function.

Fixes #140368
---
 clang/docs/ReleaseNotes.md                    |  2 +
 clang/lib/CodeGen/CGExprCXX.cpp               |  2 +-
 .../class.compare.default/p2.cpp              |  4 ++
 .../CodeGenCXX/cxx2a-three-way-comparison.cpp | 63 +++++++++++++++++++
 4 files changed, 70 insertions(+), 1 deletion(-)

diff --git a/clang/docs/ReleaseNotes.md b/clang/docs/ReleaseNotes.md
index 77a1e73bc8af60..2d60b4d66be402 100644
--- a/clang/docs/ReleaseNotes.md
+++ b/clang/docs/ReleaseNotes.md
@@ -795,6 +795,8 @@ features cannot lower the translation-unit ABI level;
 
 - Fixed a regression where the rewritten comparison operator was not instantiated properly. (#GH104720)
 
+- Fixed a crash when a defaulted comparison operator of a union with no variant members was called. (#GH140368)
+
 - Fixed an assertion failure when a parenthesized structured binding declarator
   was followed by a function declarator and body (e.g. ``([a, b])() {}``).
   (#GH218144, #GH193687)
diff --git a/clang/lib/CodeGen/CGExprCXX.cpp b/clang/lib/CodeGen/CGExprCXX.cpp
index 8279611accba33..ac895d00804e03 100644
--- a/clang/lib/CodeGen/CGExprCXX.cpp
+++ b/clang/lib/CodeGen/CGExprCXX.cpp
@@ -254,7 +254,7 @@ RValue CodeGenFunction::EmitCXXMemberOrOperatorMemberCallExpr(
   }
 
   bool TrivialForCodegen =
-      MD->isTrivial() || (MD->isDefaulted() && MD->getParent()->isUnion());
+      MD->isTrivial() || MD->isMemcpyEquivalentSpecialMember(getContext());
   bool TrivialAssignment =
       TrivialForCodegen &&
       (MD->isCopyAssignmentOperator() || MD->isMoveAssignmentOperator()) &&
diff --git a/clang/test/CXX/class/class.compare/class.compare.default/p2.cpp b/clang/test/CXX/class/class.compare/class.compare.default/p2.cpp
index 95c8f308ab604b..490ec5f11e6302 100644
--- a/clang/test/CXX/class/class.compare/class.compare.default/p2.cpp
+++ b/clang/test/CXX/class/class.compare/class.compare.default/p2.cpp
@@ -136,6 +136,10 @@ union E2 {
   bool operator>(const E2&) const = default;
   bool operator>=(const E2&) const = default;
 };
+void f(E1 e) {
+  void(e == e);
+  void(e != e);
+}
 
 struct F;
 bool operator==(const F&, const F&);
diff --git a/clang/test/CodeGenCXX/cxx2a-three-way-comparison.cpp b/clang/test/CodeGenCXX/cxx2a-three-way-comparison.cpp
index b994031a978a9f..f5e4323b583f44 100644
--- a/clang/test/CodeGenCXX/cxx2a-three-way-comparison.cpp
+++ b/clang/test/CodeGenCXX/cxx2a-three-way-comparison.cpp
@@ -70,3 +70,66 @@ void builtin_cmp(int a) {
   // CHECK: select
   a <=> a;
 }
+
+namespace GH140368 {
+union U {
+  bool operator==(const U&) const = default;
+  std::strong_ordering operator<=>(const U&) const = default;
+};
+template<typename T> union TU {
+  bool operator==(const TU&) const = default;
+};
+
+// ITANIUM-LABEL: define {{.*}}@_ZN8GH1403682eqERKNS_1UES2_(
+// MSABI-LABEL: define {{.*}}@"?eq at GH140368@@YA_NAEBTU at 1@0 at Z"(
+bool eq(const U &a, const U &b) {
+  // ITANIUM: %[[RET:.*]] = call {{.*}}@_ZNK8GH1403681UeqERKS0_(
+  // ITANIUM: ret i1 %[[RET]]
+  // MSABI: %[[RET:.*]] = call {{.*}}@"??8U at GH140368@@QEBA_NAEBT01@@Z"(
+  // MSABI: ret i1 %[[RET]]
+  return a == b;
+}
+
+// ITANIUM-LABEL: define linkonce_odr {{.*}}@_ZNK8GH1403681UeqERKS0_(
+// ITANIUM: ret i1 true
+// MSABI-LABEL: define linkonce_odr {{.*}}@"??8U at GH140368@@QEBA_NAEBT01@@Z"(
+// MSABI: ret i1 true
+
+// ITANIUM-LABEL: define {{.*}}@_ZN8GH1403682neERKNS_1UES2_(
+// MSABI-LABEL: define {{.*}}@"?ne at GH140368@@YA_NAEBTU at 1@0 at Z"(
+bool ne(const U &a, const U &b) {
+  // ITANIUM: %[[EQ:.*]] = call {{.*}}@_ZNK8GH1403681UeqERKS0_(
+  // ITANIUM: xor i1 %[[EQ]], true
+  // MSABI: %[[EQ:.*]] = call {{.*}}@"??8U at GH140368@@QEBA_NAEBT01@@Z"(
+  // MSABI: xor i1 %[[EQ]], true
+  return a != b;
+}
+
+// ITANIUM-LABEL: define {{.*}}@_ZN8GH1403683cmpERKNS_1UES2_(
+// MSABI-LABEL: define {{.*}}@"?cmp at GH140368@@YAHAEBTU at 1@0 at Z"(
+int cmp(const U &a, const U &b) {
+  // ITANIUM: call {{.*}}@_ZNK8GH1403681UssERKS0_(
+  // MSABI: call {{.*}}@"??__MU at GH140368@@QEBA?AUstrong_ordering at std@@AEBT01@@Z"(
+  return a <=> b;
+}
+
+// ITANIUM-LABEL: define {{.*}}@_ZN8GH1403682ltERKNS_1UES2_(
+// MSABI-LABEL: define {{.*}}@"?lt at GH140368@@YA_NAEBTU at 1@0 at Z"(
+bool lt(const U &a, const U &b) {
+  // ITANIUM: call {{.*}}@_ZNK8GH1403681UssERKS0_(
+  // ITANIUM: icmp slt
+  // MSABI: call {{.*}}@"??__MU at GH140368@@QEBA?AUstrong_ordering at std@@AEBT01@@Z"(
+  // MSABI: icmp slt
+  return a < b;
+}
+
+// ITANIUM-LABEL: define {{.*}}@_ZN8GH1403683teqERKNS_2TUIiEES3_(
+// MSABI-LABEL: define {{.*}}@"?teq at GH140368@@YA_NAEBT?$TU at H@1 at 0@Z"(
+bool teq(const TU<int> &a, const TU<int> &b) {
+  // ITANIUM: %[[RET:.*]] = call {{.*}}@_ZNK8GH1403682TUIiEeqERKS1_(
+  // ITANIUM: ret i1 %[[RET]]
+  // MSABI: %[[RET:.*]] = call {{.*}}@"??8?$TU at H@GH140368@@QEBA_NAEBT01@@Z"(
+  // MSABI: ret i1 %[[RET]]
+  return a == b;
+}
+}

>From 465f023eaffca093a6564fcd1107b65eb48579e9 Mon Sep 17 00:00:00 2001
From: Akash Manna <akash.manna.mymail at gmail.com>
Date: Thu, 22 Oct 2026 08:42:59 +0530
Subject: [PATCH 2/3] [clang][CodeGen] Use isMemcpyEquivalentSpecialMember as
 the only trivial-assignment check

Drop `TrivialForCodegen` and the "unknown trivial member function" assert: a trivial destructor is a no-op, anything `isMemcpyEquivalentSpecialMember` accepts is an aggregate copy, and everything else is an ordinary call. The helper also declines the `memcpy` for tagged pointer-field-protection classes with a non-trivial destructor, so such an assignment now calls the copy assignment operator, as the copy constructor path already does.
---
 clang/lib/CodeGen/CGExprCXX.cpp      | 44 +++++++++++-----------------
 clang/test/CodeGenCXX/pfp-memcpy.cpp | 40 ++++++++++++++-----------
 2 files changed, 40 insertions(+), 44 deletions(-)

diff --git a/clang/lib/CodeGen/CGExprCXX.cpp b/clang/lib/CodeGen/CGExprCXX.cpp
index 904bdd5638826d..f37edbb1ee16c9 100644
--- a/clang/lib/CodeGen/CGExprCXX.cpp
+++ b/clang/lib/CodeGen/CGExprCXX.cpp
@@ -253,12 +253,7 @@ RValue CodeGenFunction::EmitCXXMemberOrOperatorMemberCallExpr(
     }
   }
 
-  bool TrivialForCodegen =
-      MD->isTrivial() || MD->isMemcpyEquivalentSpecialMember(getContext());
-  bool TrivialAssignment =
-      TrivialForCodegen &&
-      (MD->isCopyAssignmentOperator() || MD->isMoveAssignmentOperator()) &&
-      !MD->getParent()->mayInsertExtraPadding();
+  bool TrivialAssignment = MD->isMemcpyEquivalentSpecialMember(getContext());
 
   // C++17 demands that we evaluate the RHS of a (possibly-compound) assignment
   // operator before the LHS.
@@ -324,29 +319,24 @@ RValue CodeGenFunction::EmitCXXMemberOrOperatorMemberCallExpr(
     return RValue::get(nullptr);
   }
 
-  if (TrivialForCodegen) {
-    if (isa<CXXDestructorDecl>(MD)) {
-      (void)getLValueForThis(); // Emit LHS for side effects.
-      return RValue::get(nullptr);
-    }
+  if (isa<CXXDestructorDecl>(MD) && MD->isTrivial()) {
+    (void)getLValueForThis(); // Emit LHS for side effects.
+    return RValue::get(nullptr);
+  }
 
-    if (TrivialAssignment) {
-      // We don't like to generate the trivial copy/move assignment operator
-      // when it isn't necessary; just produce the proper effect here.
-      LValue This = getLValueForThis(/*EmitCheckedForStore=*/true);
-
-      // It's important that we use the result of EmitCheckedLValue here rather
-      // than emitting call arguments, in order to preserve TBAA information
-      // from the RHS.
-      LValue RHS = isa<CXXOperatorCallExpr>(CE)
-                       ? TrivialAssignmentRHS
-                       : EmitCheckedLValue(*CE->arg_begin(), TCK_Load);
-      EmitAggregateAssign(This, RHS, CE->getType());
-      return RValue::get(This.getPointer(*this));
-    }
+  if (TrivialAssignment) {
+    // We don't like to generate the trivial copy/move assignment operator
+    // when it isn't necessary; just produce the proper effect here.
+    LValue This = getLValueForThis(/*EmitCheckedForStore=*/true);
 
-    assert(MD->getParent()->mayInsertExtraPadding() &&
-           "unknown trivial member function");
+    // It's important that we use the result of EmitCheckedLValue here rather
+    // than emitting call arguments, in order to preserve TBAA information
+    // from the RHS.
+    LValue RHS = isa<CXXOperatorCallExpr>(CE)
+                     ? TrivialAssignmentRHS
+                     : EmitCheckedLValue(*CE->arg_begin(), TCK_Load);
+    EmitAggregateAssign(This, RHS, CE->getType());
+    return RValue::get(This.getPointer(*this));
   }
 
   // Compute the function type we're calling.
diff --git a/clang/test/CodeGenCXX/pfp-memcpy.cpp b/clang/test/CodeGenCXX/pfp-memcpy.cpp
index 9621901c5c4c78..2e6417d145ef3c 100644
--- a/clang/test/CodeGenCXX/pfp-memcpy.cpp
+++ b/clang/test/CodeGenCXX/pfp-memcpy.cpp
@@ -13,26 +13,32 @@ struct ClassWithTrivialCopy {
 void trivial_assign(ClassWithTrivialCopy *s1, ClassWithTrivialCopy *s2) {
   // CHECK:      %0 = load ptr, ptr %s2.addr, align 8
   // CHECK-NEXT: %1 = load ptr, ptr %s1.addr, align 8
-  // CHECK-NEXT: call void @llvm.memcpy.p0.p0.i64(ptr align 8 %1, ptr align 8 %0, i64 16, i1 false)
-  // CHECK-NEXT: %2 = getelementptr inbounds i8, ptr %1, i64 0
-  // CHECK-NEXT: %3 = ptrtoint ptr %1 to i64
-  // CHECK-NEXT: %4 = call ptr @llvm.protected.field.ptr.p0(ptr %2, i64 %3, i1 true) [ "deactivation-symbol"(ptr @__pfp_ds__ZTS20ClassWithTrivialCopy.a) ]
-  // CHECK-NEXT: %5 = getelementptr inbounds i8, ptr %0, i64 0
-  // CHECK-NEXT: %6 = ptrtoint ptr %0 to i64
-  // CHECK-NEXT: %7 = call ptr @llvm.protected.field.ptr.p0(ptr %5, i64 %6, i1 true) [ "deactivation-symbol"(ptr @__pfp_ds__ZTS20ClassWithTrivialCopy.a) ]
-  // CHECK-NEXT: %8 = load ptr, ptr %7, align 8
-  // CHECK-NEXT: store ptr %8, ptr %4, align 8
-  // CHECK-NEXT: %9 = getelementptr inbounds i8, ptr %1, i64 8
-  // CHECK-NEXT: %10 = ptrtoint ptr %1 to i64
-  // CHECK-NEXT: %11 = call ptr @llvm.protected.field.ptr.p0(ptr %9, i64 %10, i1 true) [ "deactivation-symbol"(ptr @__pfp_ds__ZTS20ClassWithTrivialCopy.c) ]
-  // CHECK-NEXT: %12 = getelementptr inbounds i8, ptr %0, i64 8
-  // CHECK-NEXT: %13 = ptrtoint ptr %0 to i64
-  // CHECK-NEXT: %14 = call ptr @llvm.protected.field.ptr.p0(ptr %12, i64 %13, i1 true) [ "deactivation-symbol"(ptr @__pfp_ds__ZTS20ClassWithTrivialCopy.c) ]
-  // CHECK-NEXT: %15 = load ptr, ptr %14, align 8
-  // CHECK-NEXT: store ptr %15, ptr %11, align 8
+  // CHECK-NEXT: %call = call noundef nonnull align 8 dereferenceable(16) ptr @_ZN20ClassWithTrivialCopyaSERKS_(ptr noundef nonnull align 8 dereferenceable(16) %1, ptr noundef nonnull align 8 dereferenceable(16) %0)
   *s1 = *s2;
 }
 
+// CHECK-LABEL: define linkonce_odr noundef nonnull align 8 dereferenceable(16) ptr @_ZN20ClassWithTrivialCopyaSERKS_
+// CHECK:      %this1 = load ptr, ptr %this.addr, align 8
+// CHECK-NEXT: %1 = load ptr, ptr %.addr, align 8, !nonnull ![[#]], !align ![[#]]
+// CHECK-NEXT: %a = getelementptr inbounds nuw %struct.ClassWithTrivialCopy, ptr %1, i32 0, i32 0
+// CHECK-NEXT: %2 = ptrtoint ptr %1 to i64
+// CHECK-NEXT: %3 = call ptr @llvm.protected.field.ptr.p0(ptr %a, i64 %2, i1 true) [ "deactivation-symbol"(ptr @__pfp_ds__ZTS20ClassWithTrivialCopy.a) ]
+// CHECK-NEXT: %4 = load ptr, ptr %3, align 8
+// CHECK-NEXT: %a2 = getelementptr inbounds nuw %struct.ClassWithTrivialCopy, ptr %this1, i32 0, i32 0
+// CHECK-NEXT: %5 = ptrtoint ptr %this1 to i64
+// CHECK-NEXT: %6 = call ptr @llvm.protected.field.ptr.p0(ptr %a2, i64 %5, i1 true) [ "deactivation-symbol"(ptr @__pfp_ds__ZTS20ClassWithTrivialCopy.a) ]
+// CHECK-NEXT: store ptr %4, ptr %6, align 8
+// CHECK-NEXT: %7 = load ptr, ptr %.addr, align 8, !nonnull ![[#]], !align ![[#]]
+// CHECK-NEXT: %c = getelementptr inbounds nuw %struct.ClassWithTrivialCopy, ptr %7, i32 0, i32 1
+// CHECK-NEXT: %8 = ptrtoint ptr %7 to i64
+// CHECK-NEXT: %9 = call ptr @llvm.protected.field.ptr.p0(ptr %c, i64 %8, i1 true) [ "deactivation-symbol"(ptr @__pfp_ds__ZTS20ClassWithTrivialCopy.c) ]
+// CHECK-NEXT: %10 = load ptr, ptr %9, align 8
+// CHECK-NEXT: %c3 = getelementptr inbounds nuw %struct.ClassWithTrivialCopy, ptr %this1, i32 0, i32 1
+// CHECK-NEXT: %11 = ptrtoint ptr %this1 to i64
+// CHECK-NEXT: %12 = call ptr @llvm.protected.field.ptr.p0(ptr %c3, i64 %11, i1 true) [ "deactivation-symbol"(ptr @__pfp_ds__ZTS20ClassWithTrivialCopy.c) ]
+// CHECK-NEXT: store ptr %10, ptr %12, align 8
+// CHECK-NEXT: ret ptr %this1
+
 void trivial_copy(ClassWithTrivialCopy *s1) {
   ClassWithTrivialCopy s2(*s1);
 }

>From 68c80e55434d5a4280016a03f5a24c76bf2865c9 Mon Sep 17 00:00:00 2001
From: Akash Manna <akash.manna.mymail at gmail.com>
Date: Thu, 22 Oct 2026 10:31:10 +0530
Subject: [PATCH 3/3] [clang][CodeGen] Update pfp-coerce.cpp for the copy
 assignment call

A tagged PFP class with a non-trivial destructor is no longer memcpy'd on assignment, so pass_trivial_abi_pointer now calls the copy assignment operator, matching the copy constructor call the test already expects in return_trivial_abi_pointer.
---
 clang/test/CodeGenCXX/pfp-coerce.cpp | 10 +---------
 1 file changed, 1 insertion(+), 9 deletions(-)

diff --git a/clang/test/CodeGenCXX/pfp-coerce.cpp b/clang/test/CodeGenCXX/pfp-coerce.cpp
index 60f8ef5a8242f3..31158d3c8e6eea 100644
--- a/clang/test/CodeGenCXX/pfp-coerce.cpp
+++ b/clang/test/CodeGenCXX/pfp-coerce.cpp
@@ -124,15 +124,7 @@ void pass_trivial_abi_pointer(TrivialAbiPointer p, TrivialAbiPointer *pp) {
   // CHECK: store ptr %p.coerce, ptr %2, align 8
   // CHECK: store ptr %pp, ptr %pp.addr, align 8
   // CHECK: %3 = load ptr, ptr %pp.addr, align 8
-  // CHECK: call void @llvm.memcpy.p0.p0.i64(ptr align 8 %3, ptr align 8 %p, i64 8, i1 false)
-  // CHECK: %4 = getelementptr inbounds i8, ptr %3, i64 0
-  // CHECK: %5 = ptrtoint ptr %3 to i64
-  // CHECK: %6 = call ptr @llvm.protected.field.ptr.p0(ptr %4, i64 %5, i1 true) [ "deactivation-symbol"(ptr @__pfp_ds__ZTS17TrivialAbiPointer.ptr) ]
-  // CHECK: %7 = getelementptr inbounds i8, ptr %p, i64 0
-  // CHECK: %8 = ptrtoint ptr %p to i64
-  // CHECK: %9 = call ptr @llvm.protected.field.ptr.p0(ptr %7, i64 %8, i1 true) [ "deactivation-symbol"(ptr @__pfp_ds__ZTS17TrivialAbiPointer.ptr) ]
-  // CHECK: %10 = load ptr, ptr %9, align 8
-  // CHECK: store ptr %10, ptr %6, align 8
+  // CHECK: %call = call noundef nonnull align 8 dereferenceable(8) ptr @_ZN17TrivialAbiPointeraSERKS_(ptr noundef nonnull align 8 dereferenceable(8) %3, ptr noundef nonnull align 8 dereferenceable(8) %p)
   // CHECK: call void @_ZN17TrivialAbiPointerD1Ev(ptr noundef nonnull align 8 dead_on_return(8) dereferenceable(8) %p)
 
   *pp = p;



More information about the cfe-commits mailing list