[clang] [Clang][Sema] Synthesize a memcpy body for defaulted union assignment (PR #206579)
Adam Smith via cfe-commits
cfe-commits at lists.llvm.org
Fri Jul 31 09:54:15 PDT 2026
https://github.com/adams381 updated https://github.com/llvm/llvm-project/pull/206579
>From eec608cd00de5e9ecaba8bfe362b5684978404e4 Mon Sep 17 00:00:00 2001
From: Adam Smith <adams at nvidia.com>
Date: Mon, 29 Jun 2026 13:14:15 -0700
Subject: [PATCH 1/9] [Clang][Sema] Synthesize a memcpy body for defaulted
union assignment
A defaulted copy or move assignment operator for a union was left with an
empty body: the memberwise loop in DefineImplicitCopyAssignment /
DefineImplicitMoveAssignment skips union members, and the implied copy of the
object representation had no AST representation (the long-standing FIXME at the
union skip). The operator therefore copied nothing. Classic CodeGen hides
this at ordinary call sites by lowering a trivial assignment to a call-site
memcpy, but when the operator is genuinely called -- e.g. through a
pointer-to-member -- it silently copies nothing. ClangIR, which calls the
operator at the call site, hits the empty body directly and miscompiles union
assignments.
Mirror the defaulted union copy constructor (CGClass.cpp's "union copy
constructor, we must emit a memcpy") in the AST: when the class is a union,
emit a single whole-object copy through buildMemcpyForAssignmentOp -- the same
helper already used for trivially-copyable array members -- instead of the
skipped per-member assignments. The operator stays trivial (triviality is
fixed at declaration time, before the body is synthesized), so constant
evaluation, which copies trivial unions through its own semantic path and does
not execute the body, is unaffected.
The classic backend now emits the copy when the operator is odr-used, and
ClangIR's union assignment lowers to a real memcpy.
---
clang/lib/Sema/SemaDeclCXX.cpp | 40 +++++++++++++++--
.../AST/ast-dump-union-copy-move-assign.cpp | 26 +++++++++++
.../CodeGen/union-copy-move-assignment.cpp | 36 ++++++++++++++++
.../CodeGenCXX/union-copy-move-assignment.cpp | 43 +++++++++++++++++++
4 files changed, 141 insertions(+), 4 deletions(-)
create mode 100644 clang/test/AST/ast-dump-union-copy-move-assign.cpp
create mode 100644 clang/test/CIR/CodeGen/union-copy-move-assignment.cpp
create mode 100644 clang/test/CodeGenCXX/union-copy-move-assignment.cpp
diff --git a/clang/lib/Sema/SemaDeclCXX.cpp b/clang/lib/Sema/SemaDeclCXX.cpp
index e5a5963db27c2..fbdd737abbf2c 100644
--- a/clang/lib/Sema/SemaDeclCXX.cpp
+++ b/clang/lib/Sema/SemaDeclCXX.cpp
@@ -15507,10 +15507,26 @@ void Sema::DefineImplicitCopyAssignment(SourceLocation CurrentLocation,
Statements.push_back(Copy.getAs<Expr>());
}
+ // A defaulted copy assignment operator for a union copies the object
+ // representation as if by a memcpy, the same way the defaulted union copy
+ // constructor does. The memberwise loop below skips union members, so emit
+ // that whole-object copy here.
+ if (ClassDecl->isUnion()) {
+ ExprBuilder &To = ExplicitObject
+ ? static_cast<ExprBuilder &>(*ExplicitObject)
+ : static_cast<ExprBuilder &>(*DerefThis);
+ StmtResult Copy = buildMemcpyForAssignmentOp(
+ *this, Loc, Context.getCanonicalTagType(ClassDecl), To, OtherRef);
+ if (Copy.isInvalid()) {
+ CopyAssignOperator->setInvalidDecl();
+ return;
+ }
+ Statements.push_back(Copy.getAs<Stmt>());
+ }
+
// Assign non-static members.
for (auto *Field : ClassDecl->fields()) {
- // FIXME: We should form some kind of AST representation for the implied
- // memcpy in a union copy operation.
+ // Union members are copied by the whole-object memcpy emitted above.
if (Field->isUnnamedBitField() || Field->getParent()->isUnion())
continue;
@@ -15897,10 +15913,26 @@ void Sema::DefineImplicitMoveAssignment(SourceLocation CurrentLocation,
Statements.push_back(Move.getAs<Expr>());
}
+ // A defaulted move assignment operator for a union copies the object
+ // representation as if by a memcpy, the same way the defaulted union copy
+ // constructor does. The memberwise loop below skips union members, so emit
+ // that whole-object copy here.
+ if (ClassDecl->isUnion()) {
+ ExprBuilder &To = ExplicitObject
+ ? static_cast<ExprBuilder &>(*ExplicitObject)
+ : static_cast<ExprBuilder &>(*DerefThis);
+ StmtResult Move = buildMemcpyForAssignmentOp(
+ *this, Loc, Context.getCanonicalTagType(ClassDecl), To, OtherRef);
+ if (Move.isInvalid()) {
+ MoveAssignOperator->setInvalidDecl();
+ return;
+ }
+ Statements.push_back(Move.getAs<Stmt>());
+ }
+
// Assign non-static members.
for (auto *Field : ClassDecl->fields()) {
- // FIXME: We should form some kind of AST representation for the implied
- // memcpy in a union copy operation.
+ // Union members are copied by the whole-object memcpy emitted above.
if (Field->isUnnamedBitField() || Field->getParent()->isUnion())
continue;
diff --git a/clang/test/AST/ast-dump-union-copy-move-assign.cpp b/clang/test/AST/ast-dump-union-copy-move-assign.cpp
new file mode 100644
index 0000000000000..ab05bb9577916
--- /dev/null
+++ b/clang/test/AST/ast-dump-union-copy-move-assign.cpp
@@ -0,0 +1,26 @@
+// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -std=c++17 -ast-dump %s | FileCheck %s
+
+union U {
+ int a;
+ float b;
+};
+
+void odr_use(U &x, const U &y, U &&z) {
+ x = y;
+ x = static_cast<U &&>(z);
+}
+
+// The implicitly-defined defaulted union assignment operators are synthesized
+// with a whole-object __builtin_memcpy body.
+
+// CHECK: CXXMethodDecl {{.*}} implicit {{.*}}operator= 'U &(const U &)
+// CHECK: CompoundStmt
+// CHECK: CallExpr
+// CHECK: DeclRefExpr {{.*}} '__builtin_memcpy'
+// CHECK: ReturnStmt
+
+// CHECK: CXXMethodDecl {{.*}} implicit {{.*}}operator= 'U &(U &&)
+// CHECK: CompoundStmt
+// CHECK: CallExpr
+// CHECK: DeclRefExpr {{.*}} '__builtin_memcpy'
+// CHECK: ReturnStmt
diff --git a/clang/test/CIR/CodeGen/union-copy-move-assignment.cpp b/clang/test/CIR/CodeGen/union-copy-move-assignment.cpp
new file mode 100644
index 0000000000000..4f5ca5ef25a64
--- /dev/null
+++ b/clang/test/CIR/CodeGen/union-copy-move-assignment.cpp
@@ -0,0 +1,36 @@
+// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -fclangir -emit-cir %s -o %t.cir
+// RUN: FileCheck --check-prefix=CIR --input-file=%t.cir %s
+// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -fclangir -emit-llvm %s -o %t-cir.ll
+// RUN: FileCheck --check-prefix=LLVMCIR --input-file=%t-cir.ll %s
+// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -emit-llvm %s -o %t.ll
+// RUN: FileCheck --check-prefix=OGCG --input-file=%t.ll %s
+
+union U {
+ int a;
+ float b;
+};
+
+// Odr-use both defaulted assignment operators out of line so their bodies are
+// emitted under both backends.
+auto get_copy = static_cast<U &(U::*)(const U &)>(&U::operator=);
+auto get_move = static_cast<U &(U::*)(U &&)>(&U::operator=);
+
+// The defaulted union copy/move assignment operators copy the object
+// representation; CIR lowers that to a memcpy. LLVM lowering uses a memcpy
+// libcall; the classic backend uses the llvm.memcpy intrinsic (the divergence
+// is the pre-existing builtin-memcpy lowering, not this feature).
+
+// CIR: cir.func{{.*}}@_ZN1UaSERKS_{{.*}}cxx_assign<!rec_U, copy, trivial true>
+// CIR: cir.call @memcpy(
+// CIR: cir.func{{.*}}@_ZN1UaSEOS_{{.*}}cxx_assign<!rec_U, move, trivial true>
+// CIR: cir.call @memcpy(
+
+// LLVMCIR: define{{.*}}ptr @_ZN1UaSERKS_
+// LLVMCIR: call ptr @memcpy(ptr {{.*}}, ptr {{.*}}, i64 noundef 4)
+// LLVMCIR: define{{.*}}ptr @_ZN1UaSEOS_
+// LLVMCIR: call ptr @memcpy(ptr {{.*}}, ptr {{.*}}, i64 noundef 4)
+
+// OGCG: define{{.*}}ptr @_ZN1UaSERKS_
+// OGCG: call void @llvm.memcpy.p0.p0.i64(ptr {{.*}}, ptr {{.*}}, i64 4, i1 false)
+// OGCG: define{{.*}}ptr @_ZN1UaSEOS_
+// OGCG: call void @llvm.memcpy.p0.p0.i64(ptr {{.*}}, ptr {{.*}}, i64 4, i1 false)
diff --git a/clang/test/CodeGenCXX/union-copy-move-assignment.cpp b/clang/test/CodeGenCXX/union-copy-move-assignment.cpp
new file mode 100644
index 0000000000000..4cc90e9a249e1
--- /dev/null
+++ b/clang/test/CodeGenCXX/union-copy-move-assignment.cpp
@@ -0,0 +1,43 @@
+// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -std=c++17 -emit-llvm %s -o - | FileCheck %s
+
+union U {
+ int a;
+ float b;
+};
+
+// Odr-use both defaulted assignment operators out of line so their bodies are
+// emitted (a trivial assignment at a call site is otherwise memcpy'd directly).
+auto get_copy = static_cast<U &(U::*)(const U &)>(&U::operator=);
+auto get_move = static_cast<U &(U::*)(U &&)>(&U::operator=);
+
+// CHECK-LABEL: define {{.*}} ptr @_ZN1UaSERKS_
+// CHECK: call void @llvm.memcpy.p0.p0.i64(ptr {{.*}}, ptr {{.*}}, i64 4, i1 false)
+// CHECK: ret ptr
+
+// CHECK-LABEL: define {{.*}} ptr @_ZN1UaSEOS_
+// CHECK: call void @llvm.memcpy.p0.p0.i64(ptr {{.*}}, ptr {{.*}}, i64 4, i1 false)
+// CHECK: ret ptr
+
+struct WithNamedUnion {
+ U u;
+ int x;
+};
+
+// A named union member is copied as part of the containing class's defaulted
+// assignment.
+void assign_named(WithNamedUnion *d, const WithNamedUnion *s) { *d = *s; }
+// CHECK-LABEL: define {{.*}} @_Z12assign_named
+// CHECK: call void @llvm.memcpy.p0.p0.i64(ptr {{.*}}, ptr {{.*}}, i64 8, i1 false)
+
+struct WithAnonUnion {
+ union {
+ int a;
+ float b;
+ };
+ int x;
+};
+
+// An anonymous union member is likewise copied.
+void assign_anon(WithAnonUnion *d, const WithAnonUnion *s) { *d = *s; }
+// CHECK-LABEL: define {{.*}} @_Z11assign_anon
+// CHECK: call void @llvm.memcpy.p0.p0.i64(ptr {{.*}}, ptr {{.*}}, i64 8, i1 false)
>From ab1f557d28690b4459a5d8eeea918dc6c81f6a75 Mon Sep 17 00:00:00 2001
From: Adam Smith <adams at nvidia.com>
Date: Mon, 29 Jun 2026 15:41:29 -0700
Subject: [PATCH 2/9] [Clang][Sema] Factor union assignment memcpy into a
shared helper
The defaulted copy and move assignment paths emitted identical whole-object
union memcpy blocks, differing only in which assignment operator they marked
invalid. Extract buildUnionAssignmentCopy so both call sites share it; no
functional change.
---
clang/lib/Sema/SemaDeclCXX.cpp | 70 ++++++++++++++++++----------------
1 file changed, 38 insertions(+), 32 deletions(-)
diff --git a/clang/lib/Sema/SemaDeclCXX.cpp b/clang/lib/Sema/SemaDeclCXX.cpp
index fbdd737abbf2c..ead4c06c13c53 100644
--- a/clang/lib/Sema/SemaDeclCXX.cpp
+++ b/clang/lib/Sema/SemaDeclCXX.cpp
@@ -15383,6 +15383,30 @@ static void diagnoseDeprecatedCopyOperation(Sema &S, CXXMethodDecl *CopyOp) {
}
}
+// A defaulted copy or move assignment operator for a union copies the object
+// representation as if by a memcpy, the same way the defaulted union copy
+// constructor does. The memberwise loops in DefineImplicitCopyAssignment and
+// DefineImplicitMoveAssignment skip union members, so the whole-object copy is
+// emitted here instead. Marks AssignOp invalid and returns false on failure.
+static bool buildUnionAssignmentCopy(Sema &S, SourceLocation Loc,
+ CXXRecordDecl *ClassDecl,
+ std::optional<RefBuilder> &ExplicitObject,
+ std::optional<DerefBuilder> &DerefThis,
+ const ExprBuilder &From,
+ CXXMethodDecl *AssignOp,
+ SmallVectorImpl<Stmt *> &Statements) {
+ ExprBuilder &To = ExplicitObject ? static_cast<ExprBuilder &>(*ExplicitObject)
+ : static_cast<ExprBuilder &>(*DerefThis);
+ StmtResult Copy = buildMemcpyForAssignmentOp(
+ S, Loc, S.Context.getCanonicalTagType(ClassDecl), To, From);
+ if (Copy.isInvalid()) {
+ AssignOp->setInvalidDecl();
+ return false;
+ }
+ Statements.push_back(Copy.getAs<Stmt>());
+ return true;
+}
+
void Sema::DefineImplicitCopyAssignment(SourceLocation CurrentLocation,
CXXMethodDecl *CopyAssignOperator) {
DefaultedFunctionFPFeaturesRAII RestoreFP(*this, CopyAssignOperator);
@@ -15507,22 +15531,13 @@ void Sema::DefineImplicitCopyAssignment(SourceLocation CurrentLocation,
Statements.push_back(Copy.getAs<Expr>());
}
- // A defaulted copy assignment operator for a union copies the object
- // representation as if by a memcpy, the same way the defaulted union copy
- // constructor does. The memberwise loop below skips union members, so emit
- // that whole-object copy here.
- if (ClassDecl->isUnion()) {
- ExprBuilder &To = ExplicitObject
- ? static_cast<ExprBuilder &>(*ExplicitObject)
- : static_cast<ExprBuilder &>(*DerefThis);
- StmtResult Copy = buildMemcpyForAssignmentOp(
- *this, Loc, Context.getCanonicalTagType(ClassDecl), To, OtherRef);
- if (Copy.isInvalid()) {
- CopyAssignOperator->setInvalidDecl();
- return;
- }
- Statements.push_back(Copy.getAs<Stmt>());
- }
+ // A union's defaulted copy assignment copies the whole object; see
+ // buildUnionAssignmentCopy.
+ if (ClassDecl->isUnion() &&
+ !buildUnionAssignmentCopy(*this, Loc, ClassDecl, ExplicitObject,
+ DerefThis, OtherRef, CopyAssignOperator,
+ Statements))
+ return;
// Assign non-static members.
for (auto *Field : ClassDecl->fields()) {
@@ -15913,22 +15928,13 @@ void Sema::DefineImplicitMoveAssignment(SourceLocation CurrentLocation,
Statements.push_back(Move.getAs<Expr>());
}
- // A defaulted move assignment operator for a union copies the object
- // representation as if by a memcpy, the same way the defaulted union copy
- // constructor does. The memberwise loop below skips union members, so emit
- // that whole-object copy here.
- if (ClassDecl->isUnion()) {
- ExprBuilder &To = ExplicitObject
- ? static_cast<ExprBuilder &>(*ExplicitObject)
- : static_cast<ExprBuilder &>(*DerefThis);
- StmtResult Move = buildMemcpyForAssignmentOp(
- *this, Loc, Context.getCanonicalTagType(ClassDecl), To, OtherRef);
- if (Move.isInvalid()) {
- MoveAssignOperator->setInvalidDecl();
- return;
- }
- Statements.push_back(Move.getAs<Stmt>());
- }
+ // A union's defaulted move assignment copies the whole object; see
+ // buildUnionAssignmentCopy.
+ if (ClassDecl->isUnion() &&
+ !buildUnionAssignmentCopy(*this, Loc, ClassDecl, ExplicitObject,
+ DerefThis, OtherRef, MoveAssignOperator,
+ Statements))
+ return;
// Assign non-static members.
for (auto *Field : ClassDecl->fields()) {
>From 8f80389fc1fd0b0f3af6f41029b7984f5855b4ae Mon Sep 17 00:00:00 2001
From: Adam Smith <adams at nvidia.com>
Date: Tue, 30 Jun 2026 15:09:14 -0700
Subject: [PATCH 3/9] [Clang][Sema] Cast synthesized union assignment memcpy to
void*
The whole-object memcpy synthesized for a defaulted union copy/move assignment
copies the union by its object representation, which is correct even when the
union is not trivially copyable -- the defaulted union copy constructor does the
same. But because the copy is now a real __builtin_memcpy call expression in
the AST, CheckMemaccessArguments runs on it and reports -Wnontrivial-memcall
whenever the union has a non-trivially-copyable member (for example a member
with a non-trivial destructor, which leaves the assignment trivial but the union
not trivially copyable). libc++'s variant union hits exactly this, so the
synthesized body broke any -Werror build that includes <variant>.
Cast the source and destination addresses to void* in
buildMemcpyForAssignmentOp when copying the whole union, which is the (void*)
silencing that -Wnontrivial-memcall itself recommends. The cast is an explicit
CStyleCastExpr, so it survives IgnoreParenImpCasts and CheckMemaccessArguments
sees a void pointee and skips the warning; the existing array-element callers are
unchanged. A direct user memcpy of a non-trivially-copyable union still warns.
Add a SemaCXX regression test under -Wnontrivial-memcall (the synthesized copy
stays quiet, a user memcpy of the same union still warns) and pin the void*
casts in the AST dump.
---
clang/lib/Sema/SemaDeclCXX.cpp | 29 +++++++++++++++++--
.../AST/ast-dump-union-copy-move-assign.cpp | 7 ++++-
.../union-assign-memcpy-nontrivial.cpp | 26 +++++++++++++++++
3 files changed, 59 insertions(+), 3 deletions(-)
create mode 100644 clang/test/SemaCXX/union-assign-memcpy-nontrivial.cpp
diff --git a/clang/lib/Sema/SemaDeclCXX.cpp b/clang/lib/Sema/SemaDeclCXX.cpp
index ead4c06c13c53..82fd2dc50e736 100644
--- a/clang/lib/Sema/SemaDeclCXX.cpp
+++ b/clang/lib/Sema/SemaDeclCXX.cpp
@@ -14960,9 +14960,17 @@ class SubscriptBuilder: public ExprBuilder {
/// should be copied with __builtin_memcpy rather than via explicit assignments,
/// do so. This optimization only applies for arrays of scalars, and for arrays
/// of class type where the selected copy/move-assignment operator is trivial.
+///
+/// \param SuppressMemaccessWarning casts the source and destination addresses
+/// to void pointers so that CheckMemaccessArguments does not warn. A union's
+/// defaulted assignment copies the whole object representation by memcpy (like
+/// the defaulted union copy constructor), which is correct even when the union
+/// is not trivially copyable; the cast marks the copy as intentional, matching
+/// the documented (void*) silencing of -Wnontrivial-memcall.
static StmtResult
buildMemcpyForAssignmentOp(Sema &S, SourceLocation Loc, QualType T,
- const ExprBuilder &ToB, const ExprBuilder &FromB) {
+ const ExprBuilder &ToB, const ExprBuilder &FromB,
+ bool SuppressMemaccessWarning = false) {
// Compute the size of the memory buffer to be copied.
QualType SizeType = S.Context.getSizeType();
llvm::APInt Size(S.Context.getTypeSize(SizeType),
@@ -14980,6 +14988,22 @@ buildMemcpyForAssignmentOp(Sema &S, SourceLocation Loc, QualType T,
S.Context, To, UO_AddrOf, S.Context.getPointerType(To->getType()),
VK_PRValue, OK_Ordinary, Loc, false, S.CurFPFeatureOverrides());
+ if (SuppressMemaccessWarning) {
+ // An explicit cast to void* survives IgnoreParenImpCasts, so the memaccess
+ // check sees a void pointee and skips the non-trivial-copy warning.
+ QualType ConstVoidPtr =
+ S.Context.getPointerType(S.Context.VoidTy.withConst());
+ ExprResult FromCast = S.BuildCStyleCastExpr(
+ Loc, S.Context.getTrivialTypeSourceInfo(ConstVoidPtr, Loc), Loc, From);
+ ExprResult ToCast = S.BuildCStyleCastExpr(
+ Loc, S.Context.getTrivialTypeSourceInfo(S.Context.VoidPtrTy, Loc), Loc,
+ To);
+ assert(FromCast.isUsable() && ToCast.isUsable() &&
+ "cast to void* cannot fail");
+ From = FromCast.get();
+ To = ToCast.get();
+ }
+
bool NeedsCollectableMemCpy = false;
if (auto *RD = T->getBaseElementTypeUnsafe()->getAsRecordDecl())
NeedsCollectableMemCpy = RD->hasObjectMember();
@@ -15398,7 +15422,8 @@ static bool buildUnionAssignmentCopy(Sema &S, SourceLocation Loc,
ExprBuilder &To = ExplicitObject ? static_cast<ExprBuilder &>(*ExplicitObject)
: static_cast<ExprBuilder &>(*DerefThis);
StmtResult Copy = buildMemcpyForAssignmentOp(
- S, Loc, S.Context.getCanonicalTagType(ClassDecl), To, From);
+ S, Loc, S.Context.getCanonicalTagType(ClassDecl), To, From,
+ /*SuppressMemaccessWarning=*/true);
if (Copy.isInvalid()) {
AssignOp->setInvalidDecl();
return false;
diff --git a/clang/test/AST/ast-dump-union-copy-move-assign.cpp b/clang/test/AST/ast-dump-union-copy-move-assign.cpp
index ab05bb9577916..12a1a6b30fccb 100644
--- a/clang/test/AST/ast-dump-union-copy-move-assign.cpp
+++ b/clang/test/AST/ast-dump-union-copy-move-assign.cpp
@@ -11,16 +11,21 @@ void odr_use(U &x, const U &y, U &&z) {
}
// The implicitly-defined defaulted union assignment operators are synthesized
-// with a whole-object __builtin_memcpy body.
+// with a whole-object __builtin_memcpy body whose pointer arguments are cast to
+// void* so the copy is not flagged by -Wnontrivial-memcall.
// CHECK: CXXMethodDecl {{.*}} implicit {{.*}}operator= 'U &(const U &)
// CHECK: CompoundStmt
// CHECK: CallExpr
// CHECK: DeclRefExpr {{.*}} '__builtin_memcpy'
+// CHECK: CStyleCastExpr {{.*}} 'void *'
+// CHECK: CStyleCastExpr {{.*}} 'const void *'
// CHECK: ReturnStmt
// CHECK: CXXMethodDecl {{.*}} implicit {{.*}}operator= 'U &(U &&)
// CHECK: CompoundStmt
// CHECK: CallExpr
// CHECK: DeclRefExpr {{.*}} '__builtin_memcpy'
+// CHECK: CStyleCastExpr {{.*}} 'void *'
+// CHECK: CStyleCastExpr {{.*}} 'const void *'
// CHECK: ReturnStmt
diff --git a/clang/test/SemaCXX/union-assign-memcpy-nontrivial.cpp b/clang/test/SemaCXX/union-assign-memcpy-nontrivial.cpp
new file mode 100644
index 0000000000000..9865b117c2e7c
--- /dev/null
+++ b/clang/test/SemaCXX/union-assign-memcpy-nontrivial.cpp
@@ -0,0 +1,26 @@
+// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -std=c++17 -fsyntax-only \
+// RUN: -Wnontrivial-memcall -verify %s
+
+// A union with a member that is not trivially copyable (here, a non-trivial
+// destructor) is itself not trivially copyable, yet its defaulted assignment
+// operators stay trivial and non-deleted. Their synthesized whole-object
+// memcpy body must not trip -Wnontrivial-memcall.
+
+struct NonTrivialDtor {
+ ~NonTrivialDtor();
+};
+
+union U {
+ NonTrivialDtor n;
+ int i;
+};
+
+// Odr-use both defaulted assignment operators so their bodies are synthesized.
+// The synthesized memcpy must not warn.
+auto get_copy = static_cast<U &(U::*)(const U &)>(&U::operator=);
+auto get_move = static_cast<U &(U::*)(U &&)>(&U::operator=);
+
+// A user-written memcpy of the same union is not suppressed and still warns.
+void user_memcpy(U *d, const U *s) {
+ __builtin_memcpy(d, s, sizeof(U)); // expected-warning {{first argument in call to '__builtin_memcpy' is a pointer to non-trivially copyable type 'U'}} expected-note {{explicitly cast the pointer to silence this warning}}
+}
>From 43e35484e5df9f57b8b1dd4dd406aad03efd54f4 Mon Sep 17 00:00:00 2001
From: Adam Smith <adams at nvidia.com>
Date: Thu, 23 Jul 2026 08:03:29 -0700
Subject: [PATCH 4/9] [CIR] Expand defaulted union-assignment test coverage
Add tests exercising paths the union-assignment memcpy synthesis already
handled but left uncovered: a C++23 explicit-object defaulted operator
(AST dump), constant evaluation of a defaulted union assignment, and a
tail-padded union whose whole-object copy spans the padding. Add
CHECK-NOT guards so each synthesized body keeps exactly one memcpy.
---
.../ast-dump-union-assign-explicit-object.cpp | 33 +++++++++++++++++++
.../CodeGen/union-copy-move-assignment.cpp | 2 ++
.../CodeGenCXX/union-copy-move-assignment.cpp | 16 +++++++++
clang/test/SemaCXX/union-assign-constexpr.cpp | 28 ++++++++++++++++
4 files changed, 79 insertions(+)
create mode 100644 clang/test/AST/ast-dump-union-assign-explicit-object.cpp
create mode 100644 clang/test/SemaCXX/union-assign-constexpr.cpp
diff --git a/clang/test/AST/ast-dump-union-assign-explicit-object.cpp b/clang/test/AST/ast-dump-union-assign-explicit-object.cpp
new file mode 100644
index 0000000000000..e3692a4f11fb6
--- /dev/null
+++ b/clang/test/AST/ast-dump-union-assign-explicit-object.cpp
@@ -0,0 +1,33 @@
+// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -std=c++2b -ast-dump %s | FileCheck %s
+
+union U {
+ int a;
+ float b;
+ U &operator=(this U &self, const U &) = default;
+ U &operator=(this U &self, U &&) = default;
+};
+
+void odr_use(U &x, const U &y, U &&z) {
+ x = y;
+ x = static_cast<U &&>(z);
+}
+
+// A defaulted union assignment operator written with a C++23 explicit object
+// parameter is synthesized with a whole-object __builtin_memcpy body whose
+// pointer arguments are cast to void* so -Wnontrivial-memcall stays quiet.
+
+// CHECK: CXXMethodDecl {{.*}} operator= 'U &(U &, const U &)
+// CHECK: CompoundStmt
+// CHECK: CallExpr
+// CHECK: DeclRefExpr {{.*}} '__builtin_memcpy'
+// CHECK: CStyleCastExpr {{.*}} 'void *'
+// CHECK: CStyleCastExpr {{.*}} 'const void *'
+// CHECK: ReturnStmt
+
+// CHECK: CXXMethodDecl {{.*}} operator= 'U &(U &, U &&)
+// CHECK: CompoundStmt
+// CHECK: CallExpr
+// CHECK: DeclRefExpr {{.*}} '__builtin_memcpy'
+// CHECK: CStyleCastExpr {{.*}} 'void *'
+// CHECK: CStyleCastExpr {{.*}} 'const void *'
+// CHECK: ReturnStmt
diff --git a/clang/test/CIR/CodeGen/union-copy-move-assignment.cpp b/clang/test/CIR/CodeGen/union-copy-move-assignment.cpp
index 4f5ca5ef25a64..635fb4e5b08e4 100644
--- a/clang/test/CIR/CodeGen/union-copy-move-assignment.cpp
+++ b/clang/test/CIR/CodeGen/union-copy-move-assignment.cpp
@@ -27,10 +27,12 @@ auto get_move = static_cast<U &(U::*)(U &&)>(&U::operator=);
// LLVMCIR: define{{.*}}ptr @_ZN1UaSERKS_
// LLVMCIR: call ptr @memcpy(ptr {{.*}}, ptr {{.*}}, i64 noundef 4)
+// LLVMCIR-NOT: @memcpy
// LLVMCIR: define{{.*}}ptr @_ZN1UaSEOS_
// LLVMCIR: call ptr @memcpy(ptr {{.*}}, ptr {{.*}}, i64 noundef 4)
// OGCG: define{{.*}}ptr @_ZN1UaSERKS_
// OGCG: call void @llvm.memcpy.p0.p0.i64(ptr {{.*}}, ptr {{.*}}, i64 4, i1 false)
+// OGCG-NOT: @llvm.memcpy
// OGCG: define{{.*}}ptr @_ZN1UaSEOS_
// OGCG: call void @llvm.memcpy.p0.p0.i64(ptr {{.*}}, ptr {{.*}}, i64 4, i1 false)
diff --git a/clang/test/CodeGenCXX/union-copy-move-assignment.cpp b/clang/test/CodeGenCXX/union-copy-move-assignment.cpp
index 4cc90e9a249e1..3bb19def61774 100644
--- a/clang/test/CodeGenCXX/union-copy-move-assignment.cpp
+++ b/clang/test/CodeGenCXX/union-copy-move-assignment.cpp
@@ -10,12 +10,28 @@ union U {
auto get_copy = static_cast<U &(U::*)(const U &)>(&U::operator=);
auto get_move = static_cast<U &(U::*)(U &&)>(&U::operator=);
+// Exactly one whole-object memcpy per assignment body.
// CHECK-LABEL: define {{.*}} ptr @_ZN1UaSERKS_
// CHECK: call void @llvm.memcpy.p0.p0.i64(ptr {{.*}}, ptr {{.*}}, i64 4, i1 false)
+// CHECK-NOT: memcpy
// CHECK: ret ptr
// CHECK-LABEL: define {{.*}} ptr @_ZN1UaSEOS_
// CHECK: call void @llvm.memcpy.p0.p0.i64(ptr {{.*}}, ptr {{.*}}, i64 4, i1 false)
+// CHECK-NOT: memcpy
+// CHECK: ret ptr
+
+union Padded {
+ int a;
+ char b[5];
+};
+
+// sizeof(Padded) == 8, so the whole-object copy includes the tail padding.
+auto get_copy_padded = static_cast<Padded &(Padded::*)(const Padded &)>(&Padded::operator=);
+
+// CHECK-LABEL: define {{.*}} ptr @_ZN6PaddedaSERKS_
+// CHECK: call void @llvm.memcpy.p0.p0.i64(ptr {{.*}}, ptr {{.*}}, i64 8, i1 false)
+// CHECK-NOT: memcpy
// CHECK: ret ptr
struct WithNamedUnion {
diff --git a/clang/test/SemaCXX/union-assign-constexpr.cpp b/clang/test/SemaCXX/union-assign-constexpr.cpp
new file mode 100644
index 0000000000000..3a86442820c7f
--- /dev/null
+++ b/clang/test/SemaCXX/union-assign-constexpr.cpp
@@ -0,0 +1,28 @@
+// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -std=c++17 -fsyntax-only -verify %s
+// expected-no-diagnostics
+
+// The memcpy body must not block constant evaluation of a union assignment.
+
+union U {
+ int a;
+ float b;
+};
+
+constexpr int copy_active() {
+ U x{};
+ x.a = 7;
+ U y{};
+ y = x;
+ return y.a;
+}
+
+constexpr int move_active() {
+ U x{};
+ x.a = 9;
+ U y{};
+ y = static_cast<U &&>(x);
+ return y.a;
+}
+
+static_assert(copy_active() == 7);
+static_assert(move_active() == 9);
>From 6a31b7a37b3c46358a093a34b55e49a7ba3ccf4d Mon Sep 17 00:00:00 2001
From: Adam Smith <adams at nvidia.com>
Date: Thu, 23 Jul 2026 08:49:03 -0700
Subject: [PATCH 5/9] [CIR] Lower defaulted union copy/move assignment
A defaulted union copy/move assignment previously hit an errorNYI guard
in emitImplicitAssignmentOperatorBody: the synthesized body was empty
because Sema skipped union fields, so lowering it would have silently
dropped the whole-object copy.
With Sema now synthesizing a whole-object memcpy body for these
operators earlier in this PR, the guard is obsolete and blocks the
valid body from lowering. Remove it so the body lowers to a
cir.call @memcpy, matching classic CodeGen. Delete
trivial-union-assign-nyi.cpp; its behavior is now covered by
union-copy-move-assignment.cpp, which checks the emitted memcpy.
---
clang/lib/CIR/CodeGen/CIRGenClass.cpp | 14 --------------
.../test/CIR/CodeGen/trivial-union-assign-nyi.cpp | 15 ---------------
2 files changed, 29 deletions(-)
delete mode 100644 clang/test/CIR/CodeGen/trivial-union-assign-nyi.cpp
diff --git a/clang/lib/CIR/CodeGen/CIRGenClass.cpp b/clang/lib/CIR/CodeGen/CIRGenClass.cpp
index 2e50956e1c2ee..96ece6703a512 100644
--- a/clang/lib/CIR/CodeGen/CIRGenClass.cpp
+++ b/clang/lib/CIR/CodeGen/CIRGenClass.cpp
@@ -901,20 +901,6 @@ void CIRGenFunction::emitImplicitAssignmentOperatorBody(FunctionArgList &args) {
assert(!cir::MissingFeatures::incrementProfileCounter());
assert(!cir::MissingFeatures::runCleanupsScope());
- // A defaulted union copy/move assignment has an empty synthesized body:
- // Sema skips union fields (the FIXME in SemaDeclCXX::buildSingleCopyAssign),
- // so there is no AST expression for the implied whole-object memcpy.
- // Emitting that body would silently drop the copy, so report NYI instead.
- // Struct/array memcpy-equivalent assignments carry the implicit memberwise
- // copies in the AST (per-field assignment expressions, or a builtin memcpy
- // call for array members) and lower correctly through the loop below.
- if (assignOp->isMemcpyEquivalentSpecialMember(getContext()) &&
- assignOp->getParent()->isUnion()) {
- cgm.errorNYI(assignOp->getSourceRange(),
- "defaulted union copy/move assignment operator");
- return;
- }
-
// Classic codegen uses a special class to attempt to replace member
// initializers with memcpy. We could possibly defer that to the
// lowering or optimization phases to keep the memory accesses more
diff --git a/clang/test/CIR/CodeGen/trivial-union-assign-nyi.cpp b/clang/test/CIR/CodeGen/trivial-union-assign-nyi.cpp
deleted file mode 100644
index e457dca4fd23d..0000000000000
--- a/clang/test/CIR/CodeGen/trivial-union-assign-nyi.cpp
+++ /dev/null
@@ -1,15 +0,0 @@
-// RUN: %clang_cc1 -std=c++11 -triple x86_64-unknown-linux-gnu -fclangir -emit-cir -verify %s
-
-// The defaulted copy/move assignment operator of a union has an empty
-// synthesized body -- Sema skips union fields, leaving no AST expression for
-// the implied whole-object copy. Emitting that body would silently drop the
-// copy, so CIRGen reports NYI instead.
-
-// expected-error at +1 2 {{ClangIR code gen Not Yet Implemented: defaulted union copy/move assignment operator}}
-union U {
- void *p;
- int i;
-};
-
-void copy_assign(U &a, U &b) { a = b; }
-void move_assign(U &a, U &b) { a = static_cast<U &&>(b); }
>From a2e48c13469eaff948637ded62c0b36ac3cc60cf Mon Sep 17 00:00:00 2001
From: Adam Smith <adams at nvidia.com>
Date: Mon, 27 Jul 2026 09:23:33 -0700
Subject: [PATCH 6/9] [Clang][Sema] Replace union memcpy void* cast with scoped
skip
The defaulted union assignment memcpy silenced -Wnontrivial-memcall by
casting its pointer arguments to void* with an explicit C-style cast.
That cast drops pointee qualifiers such as the address space and injects
into the synthesized AST an explicit cast node the source never wrote.
Instead, add a Sema::SuppressMemaccessCheck flag set only across the
synthesized call through llvm::SaveAndRestore, and gate the
non-trivially-copyable record branch of CheckMemaccessArguments on it.
The arguments keep their real union pointer types, so no qualifier is
dropped.
Update the union-assign AST-dump tests to expect the typed-pointer
shape, and add -ast-print and address-space tests.
---
clang/include/clang/Sema/Sema.h | 7 ++++
clang/lib/Sema/SemaChecking.cpp | 3 +-
clang/lib/Sema/SemaDeclCXX.cpp | 38 ++++++++-----------
.../ast-dump-union-assign-address-space.clcpp | 22 +++++++++++
.../ast-dump-union-assign-explicit-object.cpp | 16 ++++----
.../AST/ast-dump-union-copy-move-assign.cpp | 16 ++++----
clang/test/AST/ast-print-union-assign.cpp | 21 ++++++++++
.../union-assign-memcpy-nontrivial.cpp | 5 ---
8 files changed, 86 insertions(+), 42 deletions(-)
create mode 100644 clang/test/AST/ast-dump-union-assign-address-space.clcpp
create mode 100644 clang/test/AST/ast-print-union-assign.cpp
diff --git a/clang/include/clang/Sema/Sema.h b/clang/include/clang/Sema/Sema.h
index 8a30f6319bcef..7a620b60fde1a 100644
--- a/clang/include/clang/Sema/Sema.h
+++ b/clang/include/clang/Sema/Sema.h
@@ -2643,6 +2643,13 @@ class Sema final : public SemaBase {
isConstantEvaluatedOverride;
}
+ /// Set only around the compiler-synthesized whole-object memcpy for a
+ /// defaulted union copy/move assignment. While set, CheckMemaccessArguments
+ /// skips the non-trivially-copyable record warning (warn_cxxstruct_memaccess)
+ /// for that one call, whose arguments are known correct. It does not affect
+ /// any other memaccess check or any user code.
+ bool SuppressMemaccessCheck = false;
+
SourceLocation getLocationOfStringLiteralByte(const StringLiteral *SL,
unsigned ByteNo) const;
diff --git a/clang/lib/Sema/SemaChecking.cpp b/clang/lib/Sema/SemaChecking.cpp
index ef07e8c6133ed..29a0b79ab0f19 100644
--- a/clang/lib/Sema/SemaChecking.cpp
+++ b/clang/lib/Sema/SemaChecking.cpp
@@ -11201,7 +11201,8 @@ void Sema::CheckMemaccessArguments(const CallExpr *Call,
<< ArgIdx << FnName << PointeeTy << 1);
SearchNonTrivialToCopyField::diag(PointeeTy, Dest, *this);
} else if ((BId == Builtin::BImemcpy || BId == Builtin::BImemmove) &&
- NonTriviallyCopyableCXXRecord && ArgIdx == 0) {
+ NonTriviallyCopyableCXXRecord && ArgIdx == 0 &&
+ !SuppressMemaccessCheck) {
// FIXME: Limiting this warning to dest argument until we decide
// whether it's valid for source argument too.
DiagRuntimeBehavior(Dest->getExprLoc(), Dest,
diff --git a/clang/lib/Sema/SemaDeclCXX.cpp b/clang/lib/Sema/SemaDeclCXX.cpp
index 82fd2dc50e736..29905d2c30046 100644
--- a/clang/lib/Sema/SemaDeclCXX.cpp
+++ b/clang/lib/Sema/SemaDeclCXX.cpp
@@ -14961,12 +14961,14 @@ class SubscriptBuilder: public ExprBuilder {
/// do so. This optimization only applies for arrays of scalars, and for arrays
/// of class type where the selected copy/move-assignment operator is trivial.
///
-/// \param SuppressMemaccessWarning casts the source and destination addresses
-/// to void pointers so that CheckMemaccessArguments does not warn. A union's
-/// defaulted assignment copies the whole object representation by memcpy (like
-/// the defaulted union copy constructor), which is correct even when the union
-/// is not trivially copyable; the cast marks the copy as intentional, matching
-/// the documented (void*) silencing of -Wnontrivial-memcall.
+/// \param SuppressMemaccessWarning skips the non-trivially-copyable record
+/// warning for the synthesized call by setting Sema::SuppressMemaccessCheck
+/// across it. A union's defaulted assignment copies the whole object
+/// representation by memcpy (like the defaulted union copy constructor), which
+/// is correct even when the union is not trivially copyable, so the
+/// -Wnontrivial-memcall diagnostic that CheckMemaccessArguments would emit is
+/// a false positive here. The pointer arguments keep their real types, so no
+/// qualifier is dropped.
static StmtResult
buildMemcpyForAssignmentOp(Sema &S, SourceLocation Loc, QualType T,
const ExprBuilder &ToB, const ExprBuilder &FromB,
@@ -14988,22 +14990,6 @@ buildMemcpyForAssignmentOp(Sema &S, SourceLocation Loc, QualType T,
S.Context, To, UO_AddrOf, S.Context.getPointerType(To->getType()),
VK_PRValue, OK_Ordinary, Loc, false, S.CurFPFeatureOverrides());
- if (SuppressMemaccessWarning) {
- // An explicit cast to void* survives IgnoreParenImpCasts, so the memaccess
- // check sees a void pointee and skips the non-trivial-copy warning.
- QualType ConstVoidPtr =
- S.Context.getPointerType(S.Context.VoidTy.withConst());
- ExprResult FromCast = S.BuildCStyleCastExpr(
- Loc, S.Context.getTrivialTypeSourceInfo(ConstVoidPtr, Loc), Loc, From);
- ExprResult ToCast = S.BuildCStyleCastExpr(
- Loc, S.Context.getTrivialTypeSourceInfo(S.Context.VoidPtrTy, Loc), Loc,
- To);
- assert(FromCast.isUsable() && ToCast.isUsable() &&
- "cast to void* cannot fail");
- From = FromCast.get();
- To = ToCast.get();
- }
-
bool NeedsCollectableMemCpy = false;
if (auto *RD = T->getBaseElementTypeUnsafe()->getAsRecordDecl())
NeedsCollectableMemCpy = RD->hasObjectMember();
@@ -15029,6 +15015,14 @@ buildMemcpyForAssignmentOp(Sema &S, SourceLocation Loc, QualType T,
Expr *CallArgs[] = {
To, From, IntegerLiteral::Create(S.Context, Size, SizeType, Loc)
};
+
+ // The synthesized union whole-object copy is a memcpy of a possibly
+ // non-trivially-copyable record, which is correct here; skip the memaccess
+ // warning across just this call rather than casting the arguments to void*.
+ std::optional<llvm::SaveAndRestore<bool>> DisableMemaccessCheck;
+ if (SuppressMemaccessWarning)
+ DisableMemaccessCheck.emplace(S.SuppressMemaccessCheck, true);
+
ExprResult Call = S.BuildCallExpr(/*Scope=*/nullptr, MemCpyRef.get(),
Loc, CallArgs, Loc);
diff --git a/clang/test/AST/ast-dump-union-assign-address-space.clcpp b/clang/test/AST/ast-dump-union-assign-address-space.clcpp
new file mode 100644
index 0000000000000..2974174b0d386
--- /dev/null
+++ b/clang/test/AST/ast-dump-union-assign-address-space.clcpp
@@ -0,0 +1,22 @@
+// RUN: %clang_cc1 -triple spir-unknown-unknown -cl-std=clc++2021 -x cl \
+// RUN: -ast-dump %s | FileCheck %s
+
+union U {
+ int a;
+ float b;
+};
+
+__kernel void k(__global U *g, __global const U *s) {
+ *g = *s;
+}
+
+// Typed union pointers preserve the address space; a void* cast would not.
+
+// CHECK: CXXMethodDecl {{.*}} implicit {{.*}}operator= '__generic U &(const __generic U &)
+// CHECK: CompoundStmt
+// CHECK: CallExpr
+// CHECK: DeclRefExpr {{.*}} '__builtin_memcpy'
+// CHECK: ImplicitCastExpr {{.*}} '__generic void *' <BitCast>
+// CHECK: UnaryOperator {{.*}} '__generic U *' prefix '&'
+// CHECK: ImplicitCastExpr {{.*}} 'const __generic void *' <BitCast>
+// CHECK: UnaryOperator {{.*}} 'const __generic U *' prefix '&'
diff --git a/clang/test/AST/ast-dump-union-assign-explicit-object.cpp b/clang/test/AST/ast-dump-union-assign-explicit-object.cpp
index e3692a4f11fb6..f37d3ee32103a 100644
--- a/clang/test/AST/ast-dump-union-assign-explicit-object.cpp
+++ b/clang/test/AST/ast-dump-union-assign-explicit-object.cpp
@@ -12,22 +12,24 @@ void odr_use(U &x, const U &y, U &&z) {
x = static_cast<U &&>(z);
}
-// A defaulted union assignment operator written with a C++23 explicit object
-// parameter is synthesized with a whole-object __builtin_memcpy body whose
-// pointer arguments are cast to void* so -Wnontrivial-memcall stays quiet.
+// C++23 explicit-object form uses the same typed-pointer memcpy.
// CHECK: CXXMethodDecl {{.*}} operator= 'U &(U &, const U &)
// CHECK: CompoundStmt
// CHECK: CallExpr
// CHECK: DeclRefExpr {{.*}} '__builtin_memcpy'
-// CHECK: CStyleCastExpr {{.*}} 'void *'
-// CHECK: CStyleCastExpr {{.*}} 'const void *'
+// CHECK: ImplicitCastExpr {{.*}} 'void *' <BitCast>
+// CHECK: UnaryOperator {{.*}} 'U *' prefix '&'
+// CHECK: ImplicitCastExpr {{.*}} 'const void *' <BitCast>
+// CHECK: UnaryOperator {{.*}} 'const U *' prefix '&'
// CHECK: ReturnStmt
// CHECK: CXXMethodDecl {{.*}} operator= 'U &(U &, U &&)
// CHECK: CompoundStmt
// CHECK: CallExpr
// CHECK: DeclRefExpr {{.*}} '__builtin_memcpy'
-// CHECK: CStyleCastExpr {{.*}} 'void *'
-// CHECK: CStyleCastExpr {{.*}} 'const void *'
+// CHECK: ImplicitCastExpr {{.*}} 'void *' <BitCast>
+// CHECK: UnaryOperator {{.*}} 'U *' prefix '&'
+// CHECK: ImplicitCastExpr {{.*}} 'const void *' <BitCast>
+// CHECK: UnaryOperator {{.*}} 'U *' prefix '&'
// CHECK: ReturnStmt
diff --git a/clang/test/AST/ast-dump-union-copy-move-assign.cpp b/clang/test/AST/ast-dump-union-copy-move-assign.cpp
index 12a1a6b30fccb..77ba750768d56 100644
--- a/clang/test/AST/ast-dump-union-copy-move-assign.cpp
+++ b/clang/test/AST/ast-dump-union-copy-move-assign.cpp
@@ -10,22 +10,24 @@ void odr_use(U &x, const U &y, U &&z) {
x = static_cast<U &&>(z);
}
-// The implicitly-defined defaulted union assignment operators are synthesized
-// with a whole-object __builtin_memcpy body whose pointer arguments are cast to
-// void* so the copy is not flagged by -Wnontrivial-memcall.
+// Synthesized memcpy uses typed union pointers, not a void* cast.
// CHECK: CXXMethodDecl {{.*}} implicit {{.*}}operator= 'U &(const U &)
// CHECK: CompoundStmt
// CHECK: CallExpr
// CHECK: DeclRefExpr {{.*}} '__builtin_memcpy'
-// CHECK: CStyleCastExpr {{.*}} 'void *'
-// CHECK: CStyleCastExpr {{.*}} 'const void *'
+// CHECK: ImplicitCastExpr {{.*}} 'void *' <BitCast>
+// CHECK: UnaryOperator {{.*}} 'U *' prefix '&'
+// CHECK: ImplicitCastExpr {{.*}} 'const void *' <BitCast>
+// CHECK: UnaryOperator {{.*}} 'const U *' prefix '&'
// CHECK: ReturnStmt
// CHECK: CXXMethodDecl {{.*}} implicit {{.*}}operator= 'U &(U &&)
// CHECK: CompoundStmt
// CHECK: CallExpr
// CHECK: DeclRefExpr {{.*}} '__builtin_memcpy'
-// CHECK: CStyleCastExpr {{.*}} 'void *'
-// CHECK: CStyleCastExpr {{.*}} 'const void *'
+// CHECK: ImplicitCastExpr {{.*}} 'void *' <BitCast>
+// CHECK: UnaryOperator {{.*}} 'U *' prefix '&'
+// CHECK: ImplicitCastExpr {{.*}} 'const void *' <BitCast>
+// CHECK: UnaryOperator {{.*}} 'U *' prefix '&'
// CHECK: ReturnStmt
diff --git a/clang/test/AST/ast-print-union-assign.cpp b/clang/test/AST/ast-print-union-assign.cpp
new file mode 100644
index 0000000000000..ba158804e8802
--- /dev/null
+++ b/clang/test/AST/ast-print-union-assign.cpp
@@ -0,0 +1,21 @@
+// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -std=c++17 -ast-print %s | FileCheck %s
+
+union U {
+ int a;
+ float b;
+ U &operator=(const U &) = default;
+ U &operator=(U &&) = default;
+};
+
+void odr_use(U &x, const U &y, U &&z) {
+ x = y;
+ x = static_cast<U &&>(z);
+}
+
+// The synthesized memcpy body must not leak into -ast-print.
+
+// CHECK: union U {
+// CHECK: U &operator=(const U &) noexcept = default;
+// CHECK: U &operator=(U &&) noexcept = default;
+// CHECK-NOT: __builtin_memcpy
+// CHECK-NOT: (void *)
diff --git a/clang/test/SemaCXX/union-assign-memcpy-nontrivial.cpp b/clang/test/SemaCXX/union-assign-memcpy-nontrivial.cpp
index 9865b117c2e7c..06ff501a77fe8 100644
--- a/clang/test/SemaCXX/union-assign-memcpy-nontrivial.cpp
+++ b/clang/test/SemaCXX/union-assign-memcpy-nontrivial.cpp
@@ -1,11 +1,6 @@
// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -std=c++17 -fsyntax-only \
// RUN: -Wnontrivial-memcall -verify %s
-// A union with a member that is not trivially copyable (here, a non-trivial
-// destructor) is itself not trivially copyable, yet its defaulted assignment
-// operators stay trivial and non-deleted. Their synthesized whole-object
-// memcpy body must not trip -Wnontrivial-memcall.
-
struct NonTrivialDtor {
~NonTrivialDtor();
};
>From d74647c2e5636ec23ebaddb67d5780d8951784c2 Mon Sep 17 00:00:00 2001
From: Adam Smith <adams at nvidia.com>
Date: Wed, 29 Jul 2026 11:31:52 -0700
Subject: [PATCH 7/9] [Clang][Sema] Ignore warnings across synthesized union
memcpy
A defaulted union copy/move assignment synthesizes a whole-object
__builtin_memcpy, which is correct even for a non-trivially-copyable
union. CheckMemaccessArguments would emit warn_cxxstruct_memaccess
(-Wnontrivial-memcall) for that synthesized call, a false positive.
Silence it by ignoring warnings across just the synthesized BuildCallExpr
via a new IgnoreAllWarningDiagRAII, instead of the earlier
Sema::SuppressMemaccessCheck flag that Sema owners rejected as unsound and
too narrow. The memaccess warning is deferred through
DiagRuntimeBehavior -> DiagIfReachable and flushed only when the
SynthesizedFunctionScope pops, after the RAII is gone, so DiagIfReachable
now honors the ignore-all-warnings state at enqueue time and also drops a
note that trails a skipped warning, matching how the engine drops a note
whose parent warning was ignored.
The pointer arguments keep their real types, so no qualifier is dropped,
and -Wdeprecated-copy-with-user-provided-dtor still fires for a union with
a user-provided destructor.
---
clang/docs/ReleaseNotes.md | 6 +++++
clang/include/clang/Basic/Diagnostic.h | 16 +++++++++++++
clang/include/clang/Sema/Sema.h | 16 +++++++------
clang/lib/Sema/SemaChecking.cpp | 3 +--
clang/lib/Sema/SemaDeclCXX.cpp | 23 ++++++++++---------
clang/lib/Sema/SemaExpr.cpp | 18 +++++++++++++++
.../union-assign-memcpy-nontrivial.cpp | 14 ++++++++++-
7 files changed, 75 insertions(+), 21 deletions(-)
diff --git a/clang/docs/ReleaseNotes.md b/clang/docs/ReleaseNotes.md
index 639f7b09ae002..6ee7ccf17a74a 100644
--- a/clang/docs/ReleaseNotes.md
+++ b/clang/docs/ReleaseNotes.md
@@ -368,6 +368,12 @@ features cannot lower the translation-unit ABI level;
operator required an access check that ran while an enclosing declaration
was still being parsed. (#GH210692)
+- A defaulted copy or move assignment operator for a union was left with an
+ empty body and copied nothing when the operator was actually called, for
+ example through a pointer to member. Clang now synthesizes a whole-object
+ copy so the union's object representation is copied, matching the defaulted
+ union copy constructor.
+
#### Bug Fixes to AST Handling
- Fixed a non-deterministic ordering of unused local typedefs that made
diff --git a/clang/include/clang/Basic/Diagnostic.h b/clang/include/clang/Basic/Diagnostic.h
index 66e79e3b4300b..147faa7dad46d 100644
--- a/clang/include/clang/Basic/Diagnostic.h
+++ b/clang/include/clang/Basic/Diagnostic.h
@@ -1113,6 +1113,22 @@ class DiagnosticErrorTrap {
}
};
+/// RAII class that temporarily sets the "ignore all warnings" state on a
+/// DiagnosticsEngine and restores the previous state on destruction. Use it to
+/// silence warnings around a self-contained region of diagnostics, such as a
+/// compiler-synthesized call whose arguments are known to be correct.
+class IgnoreAllWarningDiagRAII {
+ DiagnosticsEngine &Diag;
+ bool OldValue;
+
+public:
+ explicit IgnoreAllWarningDiagRAII(DiagnosticsEngine &Diag)
+ : Diag(Diag), OldValue(Diag.getIgnoreAllWarnings()) {
+ Diag.setIgnoreAllWarnings(true);
+ }
+ ~IgnoreAllWarningDiagRAII() { Diag.setIgnoreAllWarnings(OldValue); }
+};
+
/// The streaming interface shared between DiagnosticBuilder and
/// PartialDiagnostic. This class is not intended to be constructed directly
/// but only as base class of DiagnosticBuilder and PartialDiagnostic builder.
diff --git a/clang/include/clang/Sema/Sema.h b/clang/include/clang/Sema/Sema.h
index 7a620b60fde1a..560a9aacae703 100644
--- a/clang/include/clang/Sema/Sema.h
+++ b/clang/include/clang/Sema/Sema.h
@@ -2643,13 +2643,6 @@ class Sema final : public SemaBase {
isConstantEvaluatedOverride;
}
- /// Set only around the compiler-synthesized whole-object memcpy for a
- /// defaulted union copy/move assignment. While set, CheckMemaccessArguments
- /// skips the non-trivially-copyable record warning (warn_cxxstruct_memaccess)
- /// for that one call, whose arguments are known correct. It does not affect
- /// any other memaccess check or any user code.
- bool SuppressMemaccessCheck = false;
-
SourceLocation getLocationOfStringLiteralByte(const StringLiteral *SL,
unsigned ByteNo) const;
@@ -7260,6 +7253,15 @@ class Sema final : public SemaBase {
bool DiagIfReachable(SourceLocation Loc, ArrayRef<const Stmt *> Stmts,
const PartialDiagnostic &PD);
+ /// Tracks whether the most recently deferred diagnostic (queued by
+ /// DiagIfReachable) was skipped because warnings are ignored. Used to also
+ /// skip a trailing note that belongs to a skipped warning, mirroring how the
+ /// diagnostic engine drops a note whose parent warning was ignored. Only
+ /// meaningful for a note that immediately follows its own skipped warning
+ /// through the deferred path; it is reset on every other DiagIfReachable so a
+ /// stale value cannot suppress an unrelated note.
+ bool LastDeferredDiagIgnored = false;
+
/// Conditionally issue a diagnostic based on the current
/// evaluation context.
///
diff --git a/clang/lib/Sema/SemaChecking.cpp b/clang/lib/Sema/SemaChecking.cpp
index 29a0b79ab0f19..ef07e8c6133ed 100644
--- a/clang/lib/Sema/SemaChecking.cpp
+++ b/clang/lib/Sema/SemaChecking.cpp
@@ -11201,8 +11201,7 @@ void Sema::CheckMemaccessArguments(const CallExpr *Call,
<< ArgIdx << FnName << PointeeTy << 1);
SearchNonTrivialToCopyField::diag(PointeeTy, Dest, *this);
} else if ((BId == Builtin::BImemcpy || BId == Builtin::BImemmove) &&
- NonTriviallyCopyableCXXRecord && ArgIdx == 0 &&
- !SuppressMemaccessCheck) {
+ NonTriviallyCopyableCXXRecord && ArgIdx == 0) {
// FIXME: Limiting this warning to dest argument until we decide
// whether it's valid for source argument too.
DiagRuntimeBehavior(Dest->getExprLoc(), Dest,
diff --git a/clang/lib/Sema/SemaDeclCXX.cpp b/clang/lib/Sema/SemaDeclCXX.cpp
index 29905d2c30046..4e5c457e24b1a 100644
--- a/clang/lib/Sema/SemaDeclCXX.cpp
+++ b/clang/lib/Sema/SemaDeclCXX.cpp
@@ -14962,13 +14962,12 @@ class SubscriptBuilder: public ExprBuilder {
/// of class type where the selected copy/move-assignment operator is trivial.
///
/// \param SuppressMemaccessWarning skips the non-trivially-copyable record
-/// warning for the synthesized call by setting Sema::SuppressMemaccessCheck
-/// across it. A union's defaulted assignment copies the whole object
-/// representation by memcpy (like the defaulted union copy constructor), which
-/// is correct even when the union is not trivially copyable, so the
-/// -Wnontrivial-memcall diagnostic that CheckMemaccessArguments would emit is
-/// a false positive here. The pointer arguments keep their real types, so no
-/// qualifier is dropped.
+/// warning for the synthesized call by ignoring warnings across it. A union's
+/// defaulted assignment copies the whole object representation by memcpy (like
+/// the defaulted union copy constructor), which is correct even when the union
+/// is not trivially copyable, so the -Wnontrivial-memcall diagnostic that
+/// CheckMemaccessArguments would emit is a false positive here. The pointer
+/// arguments keep their real types, so no qualifier is dropped.
static StmtResult
buildMemcpyForAssignmentOp(Sema &S, SourceLocation Loc, QualType T,
const ExprBuilder &ToB, const ExprBuilder &FromB,
@@ -15017,11 +15016,13 @@ buildMemcpyForAssignmentOp(Sema &S, SourceLocation Loc, QualType T,
};
// The synthesized union whole-object copy is a memcpy of a possibly
- // non-trivially-copyable record, which is correct here; skip the memaccess
- // warning across just this call rather than casting the arguments to void*.
- std::optional<llvm::SaveAndRestore<bool>> DisableMemaccessCheck;
+ // non-trivially-copyable record, which is correct here. Silence warnings
+ // across just this call rather than casting the arguments to void*. The
+ // memaccess warning is deferred inside BuildCallExpr and honors the ignore
+ // state at that point, so a narrow scope here is sufficient.
+ std::optional<IgnoreAllWarningDiagRAII> IgnoreWarnings;
if (SuppressMemaccessWarning)
- DisableMemaccessCheck.emplace(S.SuppressMemaccessCheck, true);
+ IgnoreWarnings.emplace(S.Diags);
ExprResult Call = S.BuildCallExpr(/*Scope=*/nullptr, MemCpyRef.get(),
Loc, CallArgs, Loc);
diff --git a/clang/lib/Sema/SemaExpr.cpp b/clang/lib/Sema/SemaExpr.cpp
index 81cff1e374aaf..7704b84e187b7 100644
--- a/clang/lib/Sema/SemaExpr.cpp
+++ b/clang/lib/Sema/SemaExpr.cpp
@@ -21087,6 +21087,24 @@ bool Sema::DiagIfReachable(SourceLocation Loc, ArrayRef<const Stmt *> Stmts,
Decl->isFirstDecl() && !Decl->isInline())))
return false;
+ // A warning deferred here is flushed when the function scope is popped, after
+ // any ignore-all-warnings state active at this point has been restored. When
+ // warnings are being ignored (for example around a compiler-synthesized call
+ // whose arguments are known correct), skip enqueuing the warning now so it
+ // does not fire spuriously at flush time. Also drop a note that trails such
+ // a skipped warning, matching how the engine drops a note whose parent
+ // warning was ignored.
+ if (Diags.getIgnoreAllWarnings()) {
+ const IntrusiveRefCntPtr<DiagnosticIDs> &DiagIDs = Diags.getDiagnosticIDs();
+ if (DiagIDs->isWarningOrExtension(PD.getDiagID())) {
+ LastDeferredDiagIgnored = true;
+ return false;
+ }
+ if (DiagIDs->isNote(PD.getDiagID()) && LastDeferredDiagIgnored)
+ return false;
+ }
+ LastDeferredDiagIgnored = false;
+
if (Stmts.empty()) {
Diag(Loc, PD);
return true;
diff --git a/clang/test/SemaCXX/union-assign-memcpy-nontrivial.cpp b/clang/test/SemaCXX/union-assign-memcpy-nontrivial.cpp
index 06ff501a77fe8..53dec05e077b4 100644
--- a/clang/test/SemaCXX/union-assign-memcpy-nontrivial.cpp
+++ b/clang/test/SemaCXX/union-assign-memcpy-nontrivial.cpp
@@ -1,5 +1,5 @@
// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -std=c++17 -fsyntax-only \
-// RUN: -Wnontrivial-memcall -verify %s
+// RUN: -Wnontrivial-memcall -Wdeprecated-copy-with-user-provided-dtor -verify %s
struct NonTrivialDtor {
~NonTrivialDtor();
@@ -19,3 +19,15 @@ auto get_move = static_cast<U &(U::*)(U &&)>(&U::operator=);
void user_memcpy(U *d, const U *s) {
__builtin_memcpy(d, s, sizeof(U)); // expected-warning {{first argument in call to '__builtin_memcpy' is a pointer to non-trivially copyable type 'U'}} expected-note {{explicitly cast the pointer to silence this warning}}
}
+
+// The memcpy suppression is scoped to the synthesized call, so an unrelated
+// warning for the same union still fires: a user-provided destructor deprecates
+// the implicit copy assignment.
+union V {
+ int i;
+ ~V() {} // expected-warning {{definition of implicit copy assignment operator for 'V' is deprecated because it has a user-provided destructor}}
+};
+
+void use_deprecated_copy(V &a, const V &b) {
+ a = b; // expected-note {{in implicit copy assignment operator for 'V' first required here}}
+}
>From c0f34f3f6cfe586206e935c5f43723040bf6942b Mon Sep 17 00:00:00 2001
From: Adam Smith <adams at nvidia.com>
Date: Thu, 30 Jul 2026 13:06:40 -0700
Subject: [PATCH 8/9] [Clang][Sema] Simplify union memcpy warning suppression
The ignore-all-warnings RAII sat in buildMemcpyForAssignmentOp behind a
SuppressMemaccessWarning parameter defaulting to false, and of the
helper's three call sites only the union one ever passed true. Move the
RAII into buildUnionAssignmentCopy and drop the parameter. The reason
-Wnontrivial-memcall is a false positive is a fact about unions, and the
shared helper also copies arrays in non-union classes, so the
explanation belongs on the caller it is true of.
CheckMemaccessArguments queues warn_cxxstruct_memaccess and its (void*)
note through DiagRuntimeBehavior, which parks both until the enclosing
scope is analyzed, by which point the RAII has restored the previous
ignore state. DiagIfReachable now samples the ignore state as it fills
the function-scope queue and drops anything that is not error-class,
which takes the warning and its note together. Notes need their own
test because a note reaches DiagIfReachable on a separate call, so the
engine never sees the ignored parent whose level would otherwise
suppress it.
That is what the Sema::LastDeferredDiagIgnored member had been
reconstructing.
Diagnostics whose default mapping is an error need no special case here.
Under -w shouldSkipAnalysisForDecl discards the deferred queue before it
is flushed, so nothing on this path reaches the engine whatever its
mapping is.
The lowering tests also stop wildcarding the define argument lists,
which had been hiding the coerced pointer types and their attributes,
and the CIR test gains a shared LLVM prefix so only the memcpy libcall
and the intrinsic differ per backend.
---
clang/include/clang/Sema/Sema.h | 9 ------
clang/lib/Sema/SemaDeclCXX.cpp | 30 +++++--------------
clang/lib/Sema/SemaExpr.cpp | 26 +++++-----------
.../ast-dump-union-assign-address-space.clcpp | 3 +-
.../AST/ast-dump-union-copy-move-assign.cpp | 3 +-
.../CodeGen/union-copy-move-assignment.cpp | 29 ++++++++----------
.../CodeGenCXX/union-copy-move-assignment.cpp | 20 ++++++-------
7 files changed, 42 insertions(+), 78 deletions(-)
diff --git a/clang/include/clang/Sema/Sema.h b/clang/include/clang/Sema/Sema.h
index 560a9aacae703..8a30f6319bcef 100644
--- a/clang/include/clang/Sema/Sema.h
+++ b/clang/include/clang/Sema/Sema.h
@@ -7253,15 +7253,6 @@ class Sema final : public SemaBase {
bool DiagIfReachable(SourceLocation Loc, ArrayRef<const Stmt *> Stmts,
const PartialDiagnostic &PD);
- /// Tracks whether the most recently deferred diagnostic (queued by
- /// DiagIfReachable) was skipped because warnings are ignored. Used to also
- /// skip a trailing note that belongs to a skipped warning, mirroring how the
- /// diagnostic engine drops a note whose parent warning was ignored. Only
- /// meaningful for a note that immediately follows its own skipped warning
- /// through the deferred path; it is reset on every other DiagIfReachable so a
- /// stale value cannot suppress an unrelated note.
- bool LastDeferredDiagIgnored = false;
-
/// Conditionally issue a diagnostic based on the current
/// evaluation context.
///
diff --git a/clang/lib/Sema/SemaDeclCXX.cpp b/clang/lib/Sema/SemaDeclCXX.cpp
index 4e5c457e24b1a..7519be9c923da 100644
--- a/clang/lib/Sema/SemaDeclCXX.cpp
+++ b/clang/lib/Sema/SemaDeclCXX.cpp
@@ -14960,18 +14960,9 @@ class SubscriptBuilder: public ExprBuilder {
/// should be copied with __builtin_memcpy rather than via explicit assignments,
/// do so. This optimization only applies for arrays of scalars, and for arrays
/// of class type where the selected copy/move-assignment operator is trivial.
-///
-/// \param SuppressMemaccessWarning skips the non-trivially-copyable record
-/// warning for the synthesized call by ignoring warnings across it. A union's
-/// defaulted assignment copies the whole object representation by memcpy (like
-/// the defaulted union copy constructor), which is correct even when the union
-/// is not trivially copyable, so the -Wnontrivial-memcall diagnostic that
-/// CheckMemaccessArguments would emit is a false positive here. The pointer
-/// arguments keep their real types, so no qualifier is dropped.
static StmtResult
buildMemcpyForAssignmentOp(Sema &S, SourceLocation Loc, QualType T,
- const ExprBuilder &ToB, const ExprBuilder &FromB,
- bool SuppressMemaccessWarning = false) {
+ const ExprBuilder &ToB, const ExprBuilder &FromB) {
// Compute the size of the memory buffer to be copied.
QualType SizeType = S.Context.getSizeType();
llvm::APInt Size(S.Context.getTypeSize(SizeType),
@@ -15014,16 +15005,6 @@ buildMemcpyForAssignmentOp(Sema &S, SourceLocation Loc, QualType T,
Expr *CallArgs[] = {
To, From, IntegerLiteral::Create(S.Context, Size, SizeType, Loc)
};
-
- // The synthesized union whole-object copy is a memcpy of a possibly
- // non-trivially-copyable record, which is correct here. Silence warnings
- // across just this call rather than casting the arguments to void*. The
- // memaccess warning is deferred inside BuildCallExpr and honors the ignore
- // state at that point, so a narrow scope here is sufficient.
- std::optional<IgnoreAllWarningDiagRAII> IgnoreWarnings;
- if (SuppressMemaccessWarning)
- IgnoreWarnings.emplace(S.Diags);
-
ExprResult Call = S.BuildCallExpr(/*Scope=*/nullptr, MemCpyRef.get(),
Loc, CallArgs, Loc);
@@ -15416,9 +15397,14 @@ static bool buildUnionAssignmentCopy(Sema &S, SourceLocation Loc,
SmallVectorImpl<Stmt *> &Statements) {
ExprBuilder &To = ExplicitObject ? static_cast<ExprBuilder &>(*ExplicitObject)
: static_cast<ExprBuilder &>(*DerefThis);
+
+ // Copying the object representation is correct even for a union that is not
+ // trivially copyable, so -Wnontrivial-memcall is a false positive here.
+ // Ignoring warnings rather than casting the arguments to void* keeps them
+ // typed, which preserves their address space.
+ IgnoreAllWarningDiagRAII IgnoreWarnings(S.Diags);
StmtResult Copy = buildMemcpyForAssignmentOp(
- S, Loc, S.Context.getCanonicalTagType(ClassDecl), To, From,
- /*SuppressMemaccessWarning=*/true);
+ S, Loc, S.Context.getCanonicalTagType(ClassDecl), To, From);
if (Copy.isInvalid()) {
AssignOp->setInvalidDecl();
return false;
diff --git a/clang/lib/Sema/SemaExpr.cpp b/clang/lib/Sema/SemaExpr.cpp
index 7704b84e187b7..3b38d3ee81228 100644
--- a/clang/lib/Sema/SemaExpr.cpp
+++ b/clang/lib/Sema/SemaExpr.cpp
@@ -21087,30 +21087,20 @@ bool Sema::DiagIfReachable(SourceLocation Loc, ArrayRef<const Stmt *> Stmts,
Decl->isFirstDecl() && !Decl->isInline())))
return false;
- // A warning deferred here is flushed when the function scope is popped, after
- // any ignore-all-warnings state active at this point has been restored. When
- // warnings are being ignored (for example around a compiler-synthesized call
- // whose arguments are known correct), skip enqueuing the warning now so it
- // does not fire spuriously at flush time. Also drop a note that trails such
- // a skipped warning, matching how the engine drops a note whose parent
- // warning was ignored.
- if (Diags.getIgnoreAllWarnings()) {
- const IntrusiveRefCntPtr<DiagnosticIDs> &DiagIDs = Diags.getDiagnosticIDs();
- if (DiagIDs->isWarningOrExtension(PD.getDiagID())) {
- LastDeferredDiagIgnored = true;
- return false;
- }
- if (DiagIDs->isNote(PD.getDiagID()) && LastDeferredDiagIgnored)
- return false;
- }
- LastDeferredDiagIgnored = false;
-
if (Stmts.empty()) {
Diag(Loc, PD);
return true;
}
if (getCurFunction()) {
+ // This queue flushes after the function is analyzed, by which time an
+ // ignore-all-warnings region live here is gone, so sample it now. A note
+ // is not error-class either, so this also drops the notes that accompany a
+ // skipped warning. They arrive on their own call, out of reach of the
+ // engine's rule that drops a note whose warning was ignored.
+ if (Diags.getIgnoreAllWarnings() &&
+ Diags.getDiagnosticIDs()->isWarningOrExtension(PD.getDiagID()))
+ return false;
FunctionScopes.back()->PossiblyUnreachableDiags.push_back(
sema::PossiblyUnreachableDiag(PD, Loc, Stmts));
return true;
diff --git a/clang/test/AST/ast-dump-union-assign-address-space.clcpp b/clang/test/AST/ast-dump-union-assign-address-space.clcpp
index 2974174b0d386..069d375afa046 100644
--- a/clang/test/AST/ast-dump-union-assign-address-space.clcpp
+++ b/clang/test/AST/ast-dump-union-assign-address-space.clcpp
@@ -10,7 +10,8 @@ __kernel void k(__global U *g, __global const U *s) {
*g = *s;
}
-// Typed union pointers preserve the address space; a void* cast would not.
+// Converting the typed union pointers preserves the address space, which a
+// compiler-written cast to plain void * would drop.
// CHECK: CXXMethodDecl {{.*}} implicit {{.*}}operator= '__generic U &(const __generic U &)
// CHECK: CompoundStmt
diff --git a/clang/test/AST/ast-dump-union-copy-move-assign.cpp b/clang/test/AST/ast-dump-union-copy-move-assign.cpp
index 77ba750768d56..ed393420eaf3b 100644
--- a/clang/test/AST/ast-dump-union-copy-move-assign.cpp
+++ b/clang/test/AST/ast-dump-union-copy-move-assign.cpp
@@ -10,7 +10,8 @@ void odr_use(U &x, const U &y, U &&z) {
x = static_cast<U &&>(z);
}
-// Synthesized memcpy uses typed union pointers, not a void* cast.
+// The memcpy operands are typed union pointers, converted to void * only by
+// the builtin's parameter.
// CHECK: CXXMethodDecl {{.*}} implicit {{.*}}operator= 'U &(const U &)
// CHECK: CompoundStmt
diff --git a/clang/test/CIR/CodeGen/union-copy-move-assignment.cpp b/clang/test/CIR/CodeGen/union-copy-move-assignment.cpp
index 635fb4e5b08e4..aeb3461990e5c 100644
--- a/clang/test/CIR/CodeGen/union-copy-move-assignment.cpp
+++ b/clang/test/CIR/CodeGen/union-copy-move-assignment.cpp
@@ -1,9 +1,9 @@
// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -fclangir -emit-cir %s -o %t.cir
// RUN: FileCheck --check-prefix=CIR --input-file=%t.cir %s
// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -fclangir -emit-llvm %s -o %t-cir.ll
-// RUN: FileCheck --check-prefix=LLVMCIR --input-file=%t-cir.ll %s
+// RUN: FileCheck --check-prefixes=LLVM,LLVMCIR --input-file=%t-cir.ll %s
// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -emit-llvm %s -o %t.ll
-// RUN: FileCheck --check-prefix=OGCG --input-file=%t.ll %s
+// RUN: FileCheck --check-prefixes=LLVM,OGCG --input-file=%t.ll %s
union U {
int a;
@@ -15,24 +15,19 @@ union U {
auto get_copy = static_cast<U &(U::*)(const U &)>(&U::operator=);
auto get_move = static_cast<U &(U::*)(U &&)>(&U::operator=);
-// The defaulted union copy/move assignment operators copy the object
-// representation; CIR lowers that to a memcpy. LLVM lowering uses a memcpy
-// libcall; the classic backend uses the llvm.memcpy intrinsic (the divergence
-// is the pre-existing builtin-memcpy lowering, not this feature).
-
// CIR: cir.func{{.*}}@_ZN1UaSERKS_{{.*}}cxx_assign<!rec_U, copy, trivial true>
// CIR: cir.call @memcpy(
// CIR: cir.func{{.*}}@_ZN1UaSEOS_{{.*}}cxx_assign<!rec_U, move, trivial true>
// CIR: cir.call @memcpy(
-// LLVMCIR: define{{.*}}ptr @_ZN1UaSERKS_
-// LLVMCIR: call ptr @memcpy(ptr {{.*}}, ptr {{.*}}, i64 noundef 4)
-// LLVMCIR-NOT: @memcpy
-// LLVMCIR: define{{.*}}ptr @_ZN1UaSEOS_
-// LLVMCIR: call ptr @memcpy(ptr {{.*}}, ptr {{.*}}, i64 noundef 4)
+// The CIR backend calls the memcpy libcall where the classic backend emits the
+// llvm.memcpy intrinsic.
-// OGCG: define{{.*}}ptr @_ZN1UaSERKS_
-// OGCG: call void @llvm.memcpy.p0.p0.i64(ptr {{.*}}, ptr {{.*}}, i64 4, i1 false)
-// OGCG-NOT: @llvm.memcpy
-// OGCG: define{{.*}}ptr @_ZN1UaSEOS_
-// OGCG: call void @llvm.memcpy.p0.p0.i64(ptr {{.*}}, ptr {{.*}}, i64 4, i1 false)
+// LLVM: define linkonce_odr noundef nonnull align 4 dereferenceable(4) ptr @_ZN1UaSERKS_(ptr noundef nonnull align 4 dereferenceable(4) %{{.+}}, ptr noundef nonnull align 4 dereferenceable(4) %{{.+}})
+// LLVMCIR: call ptr @memcpy(ptr noundef %{{.+}}, ptr noundef %{{.+}}, i64 noundef 4)
+// LLVMCIR-NOT: call ptr @memcpy
+// OGCG: call void @llvm.memcpy.p0.p0.i64(ptr align 4 %{{.+}}, ptr align 4 %{{.+}}, i64 4, i1 false)
+// OGCG-NOT: call void @llvm.memcpy
+// LLVM: define linkonce_odr noundef nonnull align 4 dereferenceable(4) ptr @_ZN1UaSEOS_(ptr noundef nonnull align 4 dereferenceable(4) %{{.+}}, ptr noundef nonnull align 4 dereferenceable(4) %{{.+}})
+// LLVMCIR: call ptr @memcpy(ptr noundef %{{.+}}, ptr noundef %{{.+}}, i64 noundef 4)
+// OGCG: call void @llvm.memcpy.p0.p0.i64(ptr align 4 %{{.+}}, ptr align 4 %{{.+}}, i64 4, i1 false)
diff --git a/clang/test/CodeGenCXX/union-copy-move-assignment.cpp b/clang/test/CodeGenCXX/union-copy-move-assignment.cpp
index 3bb19def61774..406285cd3d353 100644
--- a/clang/test/CodeGenCXX/union-copy-move-assignment.cpp
+++ b/clang/test/CodeGenCXX/union-copy-move-assignment.cpp
@@ -11,13 +11,13 @@ auto get_copy = static_cast<U &(U::*)(const U &)>(&U::operator=);
auto get_move = static_cast<U &(U::*)(U &&)>(&U::operator=);
// Exactly one whole-object memcpy per assignment body.
-// CHECK-LABEL: define {{.*}} ptr @_ZN1UaSERKS_
-// CHECK: call void @llvm.memcpy.p0.p0.i64(ptr {{.*}}, ptr {{.*}}, i64 4, i1 false)
+// CHECK-LABEL: define linkonce_odr noundef nonnull align 4 dereferenceable(4) ptr @_ZN1UaSERKS_(ptr noundef nonnull align 4 dereferenceable(4) %{{.+}}, ptr noundef nonnull align 4 dereferenceable(4) %{{.+}})
+// CHECK: call void @llvm.memcpy.p0.p0.i64(ptr align 4 %{{.+}}, ptr align 4 %{{.+}}, i64 4, i1 false)
// CHECK-NOT: memcpy
// CHECK: ret ptr
-// CHECK-LABEL: define {{.*}} ptr @_ZN1UaSEOS_
-// CHECK: call void @llvm.memcpy.p0.p0.i64(ptr {{.*}}, ptr {{.*}}, i64 4, i1 false)
+// CHECK-LABEL: define linkonce_odr noundef nonnull align 4 dereferenceable(4) ptr @_ZN1UaSEOS_(ptr noundef nonnull align 4 dereferenceable(4) %{{.+}}, ptr noundef nonnull align 4 dereferenceable(4) %{{.+}})
+// CHECK: call void @llvm.memcpy.p0.p0.i64(ptr align 4 %{{.+}}, ptr align 4 %{{.+}}, i64 4, i1 false)
// CHECK-NOT: memcpy
// CHECK: ret ptr
@@ -29,8 +29,8 @@ union Padded {
// sizeof(Padded) == 8, so the whole-object copy includes the tail padding.
auto get_copy_padded = static_cast<Padded &(Padded::*)(const Padded &)>(&Padded::operator=);
-// CHECK-LABEL: define {{.*}} ptr @_ZN6PaddedaSERKS_
-// CHECK: call void @llvm.memcpy.p0.p0.i64(ptr {{.*}}, ptr {{.*}}, i64 8, i1 false)
+// CHECK-LABEL: define linkonce_odr noundef nonnull align 4 dereferenceable(8) ptr @_ZN6PaddedaSERKS_(ptr noundef nonnull align 4 dereferenceable(8) %{{.+}}, ptr noundef nonnull align 4 dereferenceable(8) %{{.+}})
+// CHECK: call void @llvm.memcpy.p0.p0.i64(ptr align 4 %{{.+}}, ptr align 4 %{{.+}}, i64 8, i1 false)
// CHECK-NOT: memcpy
// CHECK: ret ptr
@@ -42,8 +42,8 @@ struct WithNamedUnion {
// A named union member is copied as part of the containing class's defaulted
// assignment.
void assign_named(WithNamedUnion *d, const WithNamedUnion *s) { *d = *s; }
-// CHECK-LABEL: define {{.*}} @_Z12assign_named
-// CHECK: call void @llvm.memcpy.p0.p0.i64(ptr {{.*}}, ptr {{.*}}, i64 8, i1 false)
+// CHECK-LABEL: define dso_local void @_Z12assign_namedP14WithNamedUnionPKS_(ptr noundef %{{.+}}, ptr noundef %{{.+}})
+// CHECK: call void @llvm.memcpy.p0.p0.i64(ptr align 4 %{{.+}}, ptr align 4 %{{.+}}, i64 8, i1 false)
struct WithAnonUnion {
union {
@@ -55,5 +55,5 @@ struct WithAnonUnion {
// An anonymous union member is likewise copied.
void assign_anon(WithAnonUnion *d, const WithAnonUnion *s) { *d = *s; }
-// CHECK-LABEL: define {{.*}} @_Z11assign_anon
-// CHECK: call void @llvm.memcpy.p0.p0.i64(ptr {{.*}}, ptr {{.*}}, i64 8, i1 false)
+// CHECK-LABEL: define dso_local void @_Z11assign_anonP13WithAnonUnionPKS_(ptr noundef %{{.+}}, ptr noundef %{{.+}})
+// CHECK: call void @llvm.memcpy.p0.p0.i64(ptr align 4 %{{.+}}, ptr align 4 %{{.+}}, i64 8, i1 false)
>From 45edde234ea98a543d563effc901fe55a3678f6b Mon Sep 17 00:00:00 2001
From: Adam Smith <adams at nvidia.com>
Date: Fri, 31 Jul 2026 09:52:10 -0700
Subject: [PATCH 9/9] [Clang][Sema] Inline the union assignment memcpy helper
(NFC)
buildUnionAssignmentCopy wrapped a single buildMemcpyForAssignmentOp
call in IgnoreAllWarningDiagRAII. Its eight parameters carried only
*this and locals that DefineImplicitCopyAssignment and
DefineImplicitMoveAssignment already had, and the isInvalid check and
early return belong with the caller that owns the operator being
marked invalid.
Both union branches now build the whole-object copy in place.
---
clang/lib/Sema/SemaDeclCXX.cpp | 84 ++++++++++++++++------------------
1 file changed, 40 insertions(+), 44 deletions(-)
diff --git a/clang/lib/Sema/SemaDeclCXX.cpp b/clang/lib/Sema/SemaDeclCXX.cpp
index 7519be9c923da..47b01b913b428 100644
--- a/clang/lib/Sema/SemaDeclCXX.cpp
+++ b/clang/lib/Sema/SemaDeclCXX.cpp
@@ -15383,36 +15383,6 @@ static void diagnoseDeprecatedCopyOperation(Sema &S, CXXMethodDecl *CopyOp) {
}
}
-// A defaulted copy or move assignment operator for a union copies the object
-// representation as if by a memcpy, the same way the defaulted union copy
-// constructor does. The memberwise loops in DefineImplicitCopyAssignment and
-// DefineImplicitMoveAssignment skip union members, so the whole-object copy is
-// emitted here instead. Marks AssignOp invalid and returns false on failure.
-static bool buildUnionAssignmentCopy(Sema &S, SourceLocation Loc,
- CXXRecordDecl *ClassDecl,
- std::optional<RefBuilder> &ExplicitObject,
- std::optional<DerefBuilder> &DerefThis,
- const ExprBuilder &From,
- CXXMethodDecl *AssignOp,
- SmallVectorImpl<Stmt *> &Statements) {
- ExprBuilder &To = ExplicitObject ? static_cast<ExprBuilder &>(*ExplicitObject)
- : static_cast<ExprBuilder &>(*DerefThis);
-
- // Copying the object representation is correct even for a union that is not
- // trivially copyable, so -Wnontrivial-memcall is a false positive here.
- // Ignoring warnings rather than casting the arguments to void* keeps them
- // typed, which preserves their address space.
- IgnoreAllWarningDiagRAII IgnoreWarnings(S.Diags);
- StmtResult Copy = buildMemcpyForAssignmentOp(
- S, Loc, S.Context.getCanonicalTagType(ClassDecl), To, From);
- if (Copy.isInvalid()) {
- AssignOp->setInvalidDecl();
- return false;
- }
- Statements.push_back(Copy.getAs<Stmt>());
- return true;
-}
-
void Sema::DefineImplicitCopyAssignment(SourceLocation CurrentLocation,
CXXMethodDecl *CopyAssignOperator) {
DefaultedFunctionFPFeaturesRAII RestoreFP(*this, CopyAssignOperator);
@@ -15537,13 +15507,26 @@ void Sema::DefineImplicitCopyAssignment(SourceLocation CurrentLocation,
Statements.push_back(Copy.getAs<Expr>());
}
- // A union's defaulted copy assignment copies the whole object; see
- // buildUnionAssignmentCopy.
- if (ClassDecl->isUnion() &&
- !buildUnionAssignmentCopy(*this, Loc, ClassDecl, ExplicitObject,
- DerefThis, OtherRef, CopyAssignOperator,
- Statements))
- return;
+ // A defaulted copy assignment operator for a union copies the object
+ // representation as if by a memcpy, the same way the defaulted union copy
+ // constructor does. The memberwise loop below skips union members.
+ if (ClassDecl->isUnion()) {
+ ExprBuilder &To = ExplicitObject
+ ? static_cast<ExprBuilder &>(*ExplicitObject)
+ : static_cast<ExprBuilder &>(*DerefThis);
+ // Copying the object representation is correct even for a union that is
+ // not trivially copyable, so -Wnontrivial-memcall is a false positive
+ // here. Ignoring warnings rather than casting the arguments to void*
+ // keeps them typed, which preserves their address space.
+ IgnoreAllWarningDiagRAII IgnoreWarnings(Diags);
+ StmtResult Copy = buildMemcpyForAssignmentOp(
+ *this, Loc, Context.getCanonicalTagType(ClassDecl), To, OtherRef);
+ if (Copy.isInvalid()) {
+ CopyAssignOperator->setInvalidDecl();
+ return;
+ }
+ Statements.push_back(Copy.getAs<Stmt>());
+ }
// Assign non-static members.
for (auto *Field : ClassDecl->fields()) {
@@ -15934,13 +15917,26 @@ void Sema::DefineImplicitMoveAssignment(SourceLocation CurrentLocation,
Statements.push_back(Move.getAs<Expr>());
}
- // A union's defaulted move assignment copies the whole object; see
- // buildUnionAssignmentCopy.
- if (ClassDecl->isUnion() &&
- !buildUnionAssignmentCopy(*this, Loc, ClassDecl, ExplicitObject,
- DerefThis, OtherRef, MoveAssignOperator,
- Statements))
- return;
+ // A defaulted move assignment operator for a union copies the object
+ // representation as if by a memcpy, the same way the defaulted union copy
+ // constructor does. The memberwise loop below skips union members.
+ if (ClassDecl->isUnion()) {
+ ExprBuilder &To = ExplicitObject
+ ? static_cast<ExprBuilder &>(*ExplicitObject)
+ : static_cast<ExprBuilder &>(*DerefThis);
+ // Copying the object representation is correct even for a union that is
+ // not trivially copyable, so -Wnontrivial-memcall is a false positive
+ // here. Ignoring warnings rather than casting the arguments to void*
+ // keeps them typed, which preserves their address space.
+ IgnoreAllWarningDiagRAII IgnoreWarnings(Diags);
+ StmtResult Copy = buildMemcpyForAssignmentOp(
+ *this, Loc, Context.getCanonicalTagType(ClassDecl), To, OtherRef);
+ if (Copy.isInvalid()) {
+ MoveAssignOperator->setInvalidDecl();
+ return;
+ }
+ Statements.push_back(Copy.getAs<Stmt>());
+ }
// Assign non-static members.
for (auto *Field : ClassDecl->fields()) {
More information about the cfe-commits
mailing list