[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