[clang] [Clang] P3074R7: trivial unions & (unadopted) P3726R1: Adjustments to Union Lifetime Rules (PR #185830)
via cfe-commits
cfe-commits at lists.llvm.org
Wed Mar 11 03:09:34 PDT 2026
llvmbot wrote:
<!--LLVM PR SUMMARY COMMENT-->
@llvm/pr-subscribers-clang-codegen
Author: Peng Xie (love1angel)
<details>
<summary>Changes</summary>
---
Patch is 46.15 KiB, truncated to 20.00 KiB below, full version: https://github.com/llvm/llvm-project/pull/185830.diff
14 Files Affected:
- (modified) clang/include/clang/Basic/Builtins.td (+6)
- (modified) clang/include/clang/Basic/DiagnosticASTKinds.td (+5)
- (modified) clang/include/clang/Basic/DiagnosticSemaKinds.td (+10)
- (modified) clang/lib/AST/DeclCXX.cpp (+24-5)
- (modified) clang/lib/AST/ExprConstant.cpp (+114-5)
- (modified) clang/lib/CodeGen/CGBuiltin.cpp (+5)
- (modified) clang/lib/Frontend/InitPreprocessor.cpp (+4)
- (modified) clang/lib/Sema/SemaChecking.cpp (+53)
- (modified) clang/lib/Sema/SemaDeclCXX.cpp (+122-2)
- (modified) clang/test/CXX/special/class.ctor/p5-0x.cpp (+9-7)
- (modified) clang/test/CXX/special/class.ctor/p6-0x.cpp (+16-15)
- (modified) clang/test/CXX/special/class.dtor/p5-0x.cpp (+17-16)
- (added) clang/test/SemaCXX/cxx26-start-lifetime.cpp (+115)
- (added) clang/test/SemaCXX/cxx26-trivial-union.cpp (+247)
``````````diff
diff --git a/clang/include/clang/Basic/Builtins.td b/clang/include/clang/Basic/Builtins.td
index dd5bd689c08d2..4a8286a3db76c 100644
--- a/clang/include/clang/Basic/Builtins.td
+++ b/clang/include/clang/Basic/Builtins.td
@@ -1000,6 +1000,12 @@ def IsWithinLifetime : LangBuiltin<"CXX_LANG"> {
let Prototype = "bool(void*)";
}
+def StartLifetime : LangBuiltin<"CXX_LANG"> {
+ let Spellings = ["__builtin_start_lifetime"];
+ let Attributes = [NoThrow, CustomTypeChecking, Consteval];
+ let Prototype = "void(void*)";
+}
+
def GetVtablePointer : LangBuiltin<"CXX_LANG"> {
let Spellings = ["__builtin_get_vtable_pointer"];
let Attributes = [CustomTypeChecking, NoThrow, Const];
diff --git a/clang/include/clang/Basic/DiagnosticASTKinds.td b/clang/include/clang/Basic/DiagnosticASTKinds.td
index bde418695f647..37261fc3d6407 100644
--- a/clang/include/clang/Basic/DiagnosticASTKinds.td
+++ b/clang/include/clang/Basic/DiagnosticASTKinds.td
@@ -437,6 +437,11 @@ def err_invalid_is_within_lifetime : Note<
"a pointer to an object whose lifetime has not yet begun}1"
>;
+def err_invalid_start_lifetime : Note<
+ "'%0' cannot be called with "
+ "%select{a null pointer|a one-past-the-end pointer}1"
+>;
+
// inline asm related.
let CategoryName = "Inline Assembly Issue" in {
def err_asm_invalid_escape : Error<
diff --git a/clang/include/clang/Basic/DiagnosticSemaKinds.td b/clang/include/clang/Basic/DiagnosticSemaKinds.td
index 0c25eb2443d5e..b2f19513b6026 100644
--- a/clang/include/clang/Basic/DiagnosticSemaKinds.td
+++ b/clang/include/clang/Basic/DiagnosticSemaKinds.td
@@ -6406,6 +6406,12 @@ def note_enforce_read_only_placement : Note<"type was declared read-only here">;
def note_deleted_dtor_no_operator_delete : Note<
"virtual destructor requires an unambiguous, accessible 'operator delete'">;
+def note_deleted_dtor_default_ctor : Note<
+ "destructor of union %0 is implicitly deleted because "
+ "%select{it has no default constructor|"
+ "its default constructor is a deleted function|"
+ "overload resolution to default-initialize it is ambiguous|"
+ "its default constructor is not trivial}1">;
def note_deleted_special_member_class_subobject : Note<
"%select{default constructor of|copy constructor of|move constructor of|"
"copy assignment operator of|move assignment operator of|destructor of|"
@@ -13237,6 +13243,10 @@ def err_builtin_is_within_lifetime_invalid_arg : Error<
"%select{non-|function }0pointer argument to '__builtin_is_within_lifetime' "
"is not allowed">;
+def err_builtin_start_lifetime_invalid_arg : Error<
+ "'__builtin_start_lifetime' argument must be a pointer to a complete "
+ "implicit-lifetime aggregate type, but got %0">;
+
// A multi-component builtin type diagnostic. The first component broadly
// selects a scalar or container type (scalar, vector or matrix). The second
// component selects integer types and the third component selects
diff --git a/clang/lib/AST/DeclCXX.cpp b/clang/lib/AST/DeclCXX.cpp
index 083c53e28cb91..6cb90e701fff4 100644
--- a/clang/lib/AST/DeclCXX.cpp
+++ b/clang/lib/AST/DeclCXX.cpp
@@ -1180,7 +1180,11 @@ void CXXRecordDecl::addedMember(Decl *D) {
// C++11 [class]p5:
// A default constructor is trivial if [...] no non-static data member
// of its class has a brace-or-equal-initializer.
- data().HasTrivialSpecialMembers &= ~SMF_DefaultConstructor;
+ // P3074R7 [class.default.ctor]p3:
+ // In C++26, a union's default constructor is always trivial,
+ // even with brace-or-equal-initializers.
+ if (!(isUnion() && getASTContext().getLangOpts().CPlusPlus26))
+ data().HasTrivialSpecialMembers &= ~SMF_DefaultConstructor;
// C++11 [dcl.init.aggr]p1:
// An aggregate is a [...] class with [...] no
@@ -1239,7 +1243,11 @@ void CXXRecordDecl::addedMember(Decl *D) {
if (FieldRec->hasNonTrivialMoveAssignment())
data().DefaultedMoveAssignmentIsDeleted = true;
if (FieldRec->hasNonTrivialDestructor()) {
- data().DefaultedDestructorIsDeleted = true;
+ // P3074R7: In C++26, the destructor of a union is not deleted
+ // merely because a variant member has a non-trivial destructor.
+ // Deletion is determined later by Sema based on the new rules.
+ if (!Context.getLangOpts().CPlusPlus26)
+ data().DefaultedDestructorIsDeleted = true;
// C++20 [dcl.constexpr]p5:
// The definition of a constexpr destructor whose function-body is
// not = delete shall additionally satisfy...
@@ -1267,7 +1275,12 @@ void CXXRecordDecl::addedMember(Decl *D) {
// -- for all the non-static data members of its class that are of
// class type (or array thereof), each such class has a trivial
// default constructor.
- if (!FieldRec->hasTrivialDefaultConstructor())
+ // P3074R7 [class.default.ctor]p3:
+ // In C++26, "either X is a union or" for all non-variant
+ // non-static data members [...] each such class has a trivial
+ // default constructor.
+ if (!FieldRec->hasTrivialDefaultConstructor() &&
+ !(isUnion() && Context.getLangOpts().CPlusPlus26))
data().HasTrivialSpecialMembers &= ~SMF_DefaultConstructor;
// C++0x [class.copy]p13:
@@ -1305,9 +1318,15 @@ void CXXRecordDecl::addedMember(Decl *D) {
if (!FieldRec->hasTrivialMoveAssignment())
data().HasTrivialSpecialMembers &= ~SMF_MoveAssignment;
- if (!FieldRec->hasTrivialDestructor())
+ // P3074R7 [class.dtor]p8:
+ // In C++26, "either X is a union or" for all non-variant
+ // non-static data members [...] each such class has a trivial
+ // destructor.
+ if (!FieldRec->hasTrivialDestructor() &&
+ !(isUnion() && Context.getLangOpts().CPlusPlus26))
data().HasTrivialSpecialMembers &= ~SMF_Destructor;
- if (!FieldRec->hasTrivialDestructorForCall())
+ if (!FieldRec->hasTrivialDestructorForCall() &&
+ !(isUnion() && Context.getLangOpts().CPlusPlus26))
data().HasTrivialSpecialMembersForCall &= ~SMF_Destructor;
if (!FieldRec->hasIrrelevantDestructor())
data().HasIrrelevantDestructor = false;
diff --git a/clang/lib/AST/ExprConstant.cpp b/clang/lib/AST/ExprConstant.cpp
index 429fef0a1afa8..f3f05e37ef49b 100644
--- a/clang/lib/AST/ExprConstant.cpp
+++ b/clang/lib/AST/ExprConstant.cpp
@@ -2452,17 +2452,30 @@ static bool CheckEvaluationResult(CheckEvaluationResultKind CERK,
// expression.
if (Value.isArray()) {
QualType EltTy = Type->castAsArrayTypeUnsafe()->getElementType();
+
+ // P3726R1 [expr.const]p2: An inactive union subobject includes
+ // an element E of an array member of a union where E is not within
+ // its lifetime. Skip such elements during constituent values checking.
+ bool IsUnionArrayMember =
+ Info.getLangOpts().CPlusPlus26 && SubobjectDecl &&
+ SubobjectDecl->getDeclContext()->isRecord() &&
+ cast<RecordDecl>(SubobjectDecl->getDeclContext())->isUnion();
+
for (unsigned I = 0, N = Value.getArrayInitializedElts(); I != N; ++I) {
- if (!CheckEvaluationResult(CERK, Info, DiagLoc, EltTy,
- Value.getArrayInitializedElt(I), Kind,
+ const APValue &Elt = Value.getArrayInitializedElt(I);
+ if (IsUnionArrayMember && !Elt.hasValue())
+ continue;
+ if (!CheckEvaluationResult(CERK, Info, DiagLoc, EltTy, Elt, Kind,
SubobjectDecl, CheckedTemps))
return false;
}
if (!Value.hasArrayFiller())
return true;
- return CheckEvaluationResult(CERK, Info, DiagLoc, EltTy,
- Value.getArrayFiller(), Kind, SubobjectDecl,
- CheckedTemps);
+ const APValue &Filler = Value.getArrayFiller();
+ if (IsUnionArrayMember && !Filler.hasValue())
+ return true;
+ return CheckEvaluationResult(CERK, Info, DiagLoc, EltTy, Filler, Kind,
+ SubobjectDecl, CheckedTemps);
}
if (Value.isUnion() && Value.getUnionField()) {
return CheckEvaluationResult(
@@ -6795,6 +6808,45 @@ struct StartLifetimeOfUnionMemberHandler {
const AccessKinds StartLifetimeOfUnionMemberHandler::AccessKind;
+namespace {
+/// P3726R1: Handler for __builtin_start_lifetime.
+/// Starts the lifetime of the target object without initializing subobjects.
+struct BuiltinStartLifetimeHandler {
+ EvalInfo &Info;
+ static const AccessKinds AccessKind = AK_Construct;
+ typedef bool result_type;
+ bool failed() { return false; }
+ bool found(APValue &Subobj, QualType SubobjType) {
+ // P3726R1 [obj.lifetime]:
+ // If the object referenced by r is already within its lifetime,
+ // no effects.
+ if (Subobj.hasValue())
+ return true;
+
+ // Begin the lifetime of the object without initializing subobjects.
+ if (auto *RD = SubobjType->getAsCXXRecordDecl()) {
+ if (RD->isUnion()) {
+ Subobj = APValue((const FieldDecl *)nullptr);
+ } else {
+ Subobj = APValue(APValue::UninitStruct(), RD->getNumBases(),
+ std::distance(RD->field_begin(), RD->field_end()));
+ }
+ } else if (auto *AT = dyn_cast_or_null<ConstantArrayType>(
+ SubobjType->getAsArrayTypeUnsafe())) {
+ Subobj = APValue(APValue::UninitArray(), 0, AT->getZExtSize());
+ // Leave array filler absent — no element lifetimes started.
+ } else {
+ Subobj = APValue::IndeterminateValue();
+ }
+ return true;
+ }
+ bool found(APSInt &, QualType) { return true; }
+ bool found(APFloat &, QualType) { return true; }
+};
+} // end anonymous namespace
+
+const AccessKinds BuiltinStartLifetimeHandler::AccessKind;
+
/// Handle a builtin simple-assignment or a call to a trivial assignment
/// operator whose left-hand side might involve a union member access. If it
/// does, implicitly start the lifetime of any accessed union elements per
@@ -20446,6 +20498,8 @@ static bool EvaluateAtomic(const Expr *E, const LValue *This, APValue &Result,
// comma operator
//===----------------------------------------------------------------------===//
+static bool EvaluateBuiltinStartLifetime(EvalInfo &Info, const CallExpr *E);
+
namespace {
class VoidExprEvaluator
: public ExprEvaluatorBase<VoidExprEvaluator> {
@@ -20479,6 +20533,9 @@ class VoidExprEvaluator
case Builtin::BI__builtin_operator_delete:
return HandleOperatorDeleteCall(Info, E);
+ case Builtin::BI__builtin_start_lifetime:
+ return EvaluateBuiltinStartLifetime(Info, E);
+
default:
return false;
}
@@ -22186,3 +22243,55 @@ std::optional<bool> EvaluateBuiltinIsWithinLifetime(IntExprEvaluator &IEE,
return findSubobject(Info, E, CO, Val.getLValueDesignator(), handler);
}
} // namespace
+
+/// P3726R1: Evaluate __builtin_start_lifetime(ptr).
+/// Starts the lifetime of the object pointed to by ptr without initialization.
+/// If the object is a union member, it becomes the active member.
+static bool EvaluateBuiltinStartLifetime(EvalInfo &Info, const CallExpr *E) {
+ if (!Info.InConstantContext)
+ return false;
+
+ assert(E->getBuiltinCallee() == Builtin::BI__builtin_start_lifetime);
+ const Expr *Arg = E->getArg(0);
+ if (Arg->isValueDependent())
+ return false;
+
+ LValue Val;
+ if (!EvaluatePointer(Arg, Val, Info))
+ return false;
+
+ auto Error = [&](int Diag) {
+ bool CalledFromStd = false;
+ const auto *Callee = Info.CurrentCall->getCallee();
+ if (Callee && Callee->isInStdNamespace()) {
+ const IdentifierInfo *Identifier = Callee->getIdentifier();
+ CalledFromStd = Identifier && Identifier->isStr("start_lifetime");
+ }
+ Info.FFDiag(CalledFromStd ? Info.CurrentCall->getCallRange().getBegin()
+ : E->getExprLoc(),
+ diag::err_invalid_start_lifetime)
+ << (CalledFromStd ? "std::start_lifetime" : "__builtin_start_lifetime")
+ << Diag;
+ return false;
+ };
+
+ if (Val.isNullPointer() || Val.getLValueBase().isNull())
+ return Error(0);
+
+ if (Val.getLValueDesignator().isOnePastTheEnd())
+ return Error(1);
+
+ QualType T = Val.getLValueBase().getType();
+
+ // Find the complete object.
+ CompleteObject CO =
+ findCompleteObject(Info, E, AccessKinds::AK_Construct, Val, T);
+ if (!CO)
+ return false;
+
+ // Navigate to the target subobject. Use AK_Construct so that
+ // findSubobject will activate inactive union members along the path.
+ // The handler starts the lifetime without initializing subobjects.
+ BuiltinStartLifetimeHandler Handler{Info};
+ return findSubobject(Info, E, CO, Val.getLValueDesignator(), Handler);
+}
diff --git a/clang/lib/CodeGen/CGBuiltin.cpp b/clang/lib/CodeGen/CGBuiltin.cpp
index 6fb43d5cb0fbf..041079f0d3cf0 100644
--- a/clang/lib/CodeGen/CGBuiltin.cpp
+++ b/clang/lib/CodeGen/CGBuiltin.cpp
@@ -5642,6 +5642,11 @@ RValue CodeGenFunction::EmitBuiltinExpr(const GlobalDecl GD, unsigned BuiltinID,
E->getCallee()->getType()->castAs<FunctionProtoType>(), E, true);
return RValue::get(nullptr);
+ case Builtin::BI__builtin_start_lifetime:
+ // P3726R1: No-op at runtime. Lifetime of implicit-lifetime aggregates
+ // begins automatically with storage acquisition.
+ return RValue::get(nullptr);
+
case Builtin::BI__builtin_is_aligned:
return EmitBuiltinIsAligned(E);
case Builtin::BI__builtin_align_up:
diff --git a/clang/lib/Frontend/InitPreprocessor.cpp b/clang/lib/Frontend/InitPreprocessor.cpp
index 1ccd74314f373..d698769a8a89f 100644
--- a/clang/lib/Frontend/InitPreprocessor.cpp
+++ b/clang/lib/Frontend/InitPreprocessor.cpp
@@ -746,6 +746,10 @@ static void InitializeCPlusPlusFeatureTestMacros(const LangOptions &LangOpts,
Builder.defineMacro("__cpp_variadic_friend", "202403L");
Builder.defineMacro("__cpp_trivial_relocatability", "202502L");
+ // C++26 features.
+ if (LangOpts.CPlusPlus26)
+ Builder.defineMacro("__cpp_trivial_union", "202507L");
+
if (LangOpts.Char8)
Builder.defineMacro("__cpp_char8_t", "202207L");
Builder.defineMacro("__cpp_impl_destroying_delete", "201806L");
diff --git a/clang/lib/Sema/SemaChecking.cpp b/clang/lib/Sema/SemaChecking.cpp
index 29add9d092e6b..ff63068279c31 100644
--- a/clang/lib/Sema/SemaChecking.cpp
+++ b/clang/lib/Sema/SemaChecking.cpp
@@ -2021,6 +2021,57 @@ static ExprResult BuiltinIsWithinLifetime(Sema &S, CallExpr *TheCall) {
return TheCall;
}
+static ExprResult BuiltinStartLifetime(Sema &S, CallExpr *TheCall) {
+ if (S.checkArgCount(TheCall, 1))
+ return ExprError();
+
+ ExprResult Arg = S.DefaultFunctionArrayLvalueConversion(TheCall->getArg(0));
+ if (Arg.isInvalid())
+ return ExprError();
+ QualType ParamTy = Arg.get()->getType();
+ TheCall->setArg(0, Arg.get());
+ TheCall->setType(S.Context.VoidTy);
+
+ const auto *PT = ParamTy->getAs<PointerType>();
+ if (!PT) {
+ S.Diag(TheCall->getArg(0)->getExprLoc(),
+ diag::err_builtin_start_lifetime_invalid_arg)
+ << ParamTy;
+ return ExprError();
+ }
+
+ QualType PointeeTy = PT->getPointeeType();
+
+ // Mandates: T is a complete type
+ if (S.RequireCompleteType(TheCall->getArg(0)->getExprLoc(), PointeeTy,
+ diag::err_incomplete_type))
+ return ExprError();
+
+ // Mandates: T is an implicit-lifetime aggregate type
+ // Check aggregate first
+ if (!PointeeTy->isAggregateType()) {
+ S.Diag(TheCall->getArg(0)->getExprLoc(),
+ diag::err_builtin_start_lifetime_invalid_arg)
+ << PointeeTy;
+ return ExprError();
+ }
+
+ // Check implicit-lifetime: for aggregates, destructor must not be
+ // user-provided
+ if (const auto *RD = PointeeTy->getAsCXXRecordDecl()) {
+ if (const auto *Dtor = RD->getDestructor()) {
+ if (Dtor->isUserProvided()) {
+ S.Diag(TheCall->getArg(0)->getExprLoc(),
+ diag::err_builtin_start_lifetime_invalid_arg)
+ << PointeeTy;
+ return ExprError();
+ }
+ }
+ }
+
+ return TheCall;
+}
+
static ExprResult BuiltinTriviallyRelocate(Sema &S, CallExpr *TheCall) {
if (S.checkArgCount(TheCall, 3))
return ExprError();
@@ -3071,6 +3122,8 @@ Sema::CheckBuiltinFunctionCall(FunctionDecl *FDecl, unsigned BuiltinID,
return BuiltinLaunder(*this, TheCall);
case Builtin::BI__builtin_is_within_lifetime:
return BuiltinIsWithinLifetime(*this, TheCall);
+ case Builtin::BI__builtin_start_lifetime:
+ return BuiltinStartLifetime(*this, TheCall);
case Builtin::BI__builtin_trivially_relocate:
return BuiltinTriviallyRelocate(*this, TheCall);
diff --git a/clang/lib/Sema/SemaDeclCXX.cpp b/clang/lib/Sema/SemaDeclCXX.cpp
index 2ae6e5de0e3ee..86be35984f418 100644
--- a/clang/lib/Sema/SemaDeclCXX.cpp
+++ b/clang/lib/Sema/SemaDeclCXX.cpp
@@ -9615,6 +9615,12 @@ bool SpecialMemberDeletionInfo::shouldDeleteForSubobjectCall(
if (SMOR.getKind() == Sema::SpecialMemberOverloadResult::NoMemberOrDeleted) {
if (CSM == CXXSpecialMemberKind::DefaultConstructor && Field &&
Field->getParent()->isUnion()) {
+ // P3074R7: In C++26, a union's defaulted default constructor is never
+ // deleted due to a variant member with a non-trivial default
+ // constructor. The old [class.default.ctor]p2 union-specific bullets
+ // are removed.
+ if (S.getLangOpts().CPlusPlus26)
+ return false;
// [class.default.ctor]p2:
// A defaulted default constructor for class X is defined as deleted if
// - X is a union that has a variant member with a non-trivial default
@@ -9637,6 +9643,11 @@ bool SpecialMemberDeletionInfo::shouldDeleteForSubobjectCall(
// destructor is never actually called, but is semantically checked as
// if it were.
if (CSM == CXXSpecialMemberKind::DefaultConstructor) {
+ // P3074R7: In C++26, a union's defaulted default constructor is never
+ // deleted due to a variant member with a non-trivial default
+ // constructor.
+ if (S.getLangOpts().CPlusPlus26)
+ return false;
// [class.default.ctor]p2:
// A defaulted default constructor for class X is defined as deleted if
// - X is a union that has a variant member with a non-trivial default
@@ -9645,6 +9656,13 @@ bool SpecialMemberDeletionInfo::shouldDeleteForSubobjectCall(
const auto *RD = cast<CXXRecordDecl>(Field->getParent());
if (!RD->hasInClassInitializer())
DiagKind = NonTrivialDecl;
+ } else if (CSM == CXXSpecialMemberKind::Destructor &&
+ S.getLangOpts().CPlusPlus26) {
+ // P3074R7 [class.dtor]p7: In C++26, a union's destructor is not
+ // deleted merely because a variant member has a non-trivial destructor.
+ // Deletion is determined by the new union-specific rules in
+ // ShouldDeleteSpecialMember.
+ return false;
} else {
DiagKind = NonTrivialDecl;
}
@@ -9806,6 +9824,12 @@ bool SpecialMemberDeletionInfo::shouldDeleteForField(FieldDecl *FD) {
if (inUnion() && shouldDeleteForVariantPtrAuthMember(FD))
return true;
+ // P3074R7: In C++26, a union's defaulted default constructor is trivially
+ // defined and never deleted due to variant member properties.
+ if (inUnion() && S.getLangOpts().CPlusPlus26 &&
+ CSM == CXXSpecialMemberKind::DefaultConstructor)
+ return false;
+
if (CSM == CXXSpecialMemberKind::DefaultConstructor) {
// For a default constructor, all references must be initialized in-class
// and, if a union, it must have a non-const member.
@@ -9884,7 +9908,8 @@ bool SpecialMemberDeletionInfo::shouldDeleteForField(FieldDecl *FD) {
// At least one member in each anonymous union must be non-const
if (CSM == CXXSpecialMemberKind::DefaultConstructor &&
- AllVariantFieldsAreConst && !FieldRecord->field_empty()) {
+ AllVariantFieldsAreConst && !FieldRecord->field_empty() &&
+ !S.getLangOpts().CPlusPlus26) {
if (Diagnose)
S.Diag(FieldRecord->getLocation(),
diag::note_deleted_default_ctor_all_const)
@@ -9914,6 +9939,8 @@ bool SpecialMemberDeletionInfo::shouldDeleteForAllConstMembers() {
// default constructor. Don't do that.
...
[truncated]
``````````
</details>
https://github.com/llvm/llvm-project/pull/185830
More information about the cfe-commits
mailing list