[clang] [Clang] Track temporary cleanups in rebuilt default member initializers (PR #196597)
Yuxuan Chen via cfe-commits
cfe-commits at lists.llvm.org
Fri May 8 10:53:24 PDT 2026
https://github.com/yuxuanchen1997 created https://github.com/llvm/llvm-project/pull/196597
Fixes https://github.com/llvm/llvm-project/issues/196469
When Clang rebuilds a default member initializer for CWG1815 lifetime extension, TreeTransform's initializer path can drop CXXBindTemporaryExpr cleanup information. That loses destructor cleanup for ordinary temporaries inside the initializer; for a DMI-local lambda with an init-capture, the closure temporary is not destroyed at the end of the full-expression.
Handle CXXBindTemporaryExpr explicitly while rebuilding these initializers, rebind transformed subexpressions with MaybeBindToTemporary, and remember whether the rebuilt initializer still needs non-lifetime-extended cleanups. After discarding the cleanups collected for lifetime extension, restore the ExprWithCleanups marker only when such a rebuilt temporary remains.
When MaybeBindToTemporary references an implicit destructor and Sema has synthesized its body, pass that declaration to the AST consumer because there may be no later top-level definition point for DMI-local closure types. Add a CodeGenCXX regression test for a lambda init-capture in a default member initializer.
Assisted By: OpenAI Codex
>From b2588b33d516a7ad48ce1701879d447ff360efb3 Mon Sep 17 00:00:00 2001
From: Yuxuan Chen <ych at meta.com>
Date: Thu, 7 May 2026 20:24:49 -0700
Subject: [PATCH] [Clang] Track temporary cleanups in rebuilt default member
initializers
Fixes https://github.com/llvm/llvm-project/issues/196469
When Clang rebuilds a default member initializer for CWG1815 lifetime extension, TreeTransform's initializer path can drop CXXBindTemporaryExpr cleanup information. That loses destructor cleanup for ordinary temporaries inside the initializer; for a DMI-local lambda with an init-capture, the closure temporary is not destroyed at the end of the full-expression.
Handle CXXBindTemporaryExpr explicitly while rebuilding these initializers, rebind transformed subexpressions with MaybeBindToTemporary, and remember whether the rebuilt initializer still needs non-lifetime-extended cleanups. After discarding the cleanups collected for lifetime extension, restore the ExprWithCleanups marker only when such a rebuilt temporary remains.
When MaybeBindToTemporary references an implicit destructor and Sema has synthesized its body, pass that declaration to the AST consumer because there may be no later top-level definition point for DMI-local closure types. Add a CodeGenCXX regression test for a lambda init-capture in a default member initializer.
Assisted By: OpenAI Codex
---
clang/lib/Sema/SemaExpr.cpp | 55 ++++++++++++++++++-
clang/lib/Sema/SemaExprCXX.cpp | 9 +++
...469-default-member-init-lambda-cleanup.cpp | 24 ++++++++
3 files changed, 87 insertions(+), 1 deletion(-)
create mode 100644 clang/test/CodeGenCXX/gh196469-default-member-init-lambda-cleanup.cpp
diff --git a/clang/lib/Sema/SemaExpr.cpp b/clang/lib/Sema/SemaExpr.cpp
index 9fd8c6a0a5451..ff6ce49c0d04b 100644
--- a/clang/lib/Sema/SemaExpr.cpp
+++ b/clang/lib/Sema/SemaExpr.cpp
@@ -5703,11 +5703,54 @@ struct ImmediateCallVisitor : DynamicRecursiveASTVisitor {
struct EnsureImmediateInvocationInDefaultArgs
: TreeTransform<EnsureImmediateInvocationInDefaultArgs> {
+ using Base = TreeTransform<EnsureImmediateInvocationInDefaultArgs>;
+
EnsureImmediateInvocationInDefaultArgs(Sema &SemaRef)
: TreeTransform(SemaRef) {}
bool AlwaysRebuild() { return true; }
+ bool rebuiltInitNeedsCleanups() const { return RebuiltInitNeedsCleanups; }
+
+ ExprResult TransformCXXBindTemporaryExpr(CXXBindTemporaryExpr *E) {
+ // TransformInitializer normally strips CXXBindTemporaryExpr. In default
+ // member initializers, rebuild the binding explicitly so CodeGen still
+ // knows which rebuilt temporaries need end-of-full-expression destruction.
+ ExprResult SubExpr = Base::TransformExpr(E->getSubExpr());
+ if (SubExpr.isInvalid())
+ return ExprError();
+ if (SubExpr.get() == E->getSubExpr()) {
+ if (!SuppressRebuiltTemporaryCleanup)
+ RebuiltInitNeedsCleanups = true;
+ return E;
+ }
+
+ ExprResult Res = SemaRef.MaybeBindToTemporary(SubExpr.get());
+ if (!SuppressRebuiltTemporaryCleanup && !Res.isInvalid())
+ RebuiltInitNeedsCleanups = true;
+ return Res;
+ }
+
+ ExprResult TransformInitializer(Expr *Init, bool NotCopyInit) {
+ Expr *UnwrappedInit = Init;
+ if (auto *FE = dyn_cast_if_present<FullExpr>(UnwrappedInit))
+ UnwrappedInit = FE->getSubExpr();
+
+ if (auto *MTE =
+ dyn_cast_if_present<MaterializeTemporaryExpr>(UnwrappedInit);
+ MTE && MTE->getExtendingDecl()) {
+ // The lifetime-extended temporary is intentionally not cleaned up at the
+ // end of the default member initializer full-expression.
+ llvm::SaveAndRestore SaveSuppress(SuppressRebuiltTemporaryCleanup, true);
+ return Base::TransformInitializer(Init, NotCopyInit);
+ }
+
+ if (auto *E = dyn_cast_if_present<CXXBindTemporaryExpr>(UnwrappedInit))
+ return TransformCXXBindTemporaryExpr(E);
+
+ return Base::TransformInitializer(Init, NotCopyInit);
+ }
+
// Lambda can only have immediate invocations in the default
// args of their parameters, which is transformed upon calling the closure.
// The body is not a subexpression, so we have nothing to do.
@@ -5741,6 +5784,10 @@ struct EnsureImmediateInvocationInDefaultArgs
return getDerived().RebuildSourceLocExpr(
E->getIdentKind(), E->getType(), E->getBeginLoc(), E->getEndLoc(), DC);
}
+
+private:
+ bool RebuiltInitNeedsCleanups = false;
+ bool SuppressRebuiltTemporaryCleanup = false;
};
ExprResult Sema::BuildCXXDefaultArgExpr(SourceLocation CallLoc,
@@ -5882,6 +5929,8 @@ ExprResult Sema::BuildCXXDefaultInitExpr(SourceLocation Loc, FieldDecl *Field) {
if (!NestedDefaultChecking)
V.TraverseDecl(Field);
+ bool RebuiltInitNeedsCleanups = false;
+
// CWG1815
// Support lifetime extension of temporary created by aggregate
// initialization using a default member initializer. We should rebuild
@@ -5914,6 +5963,7 @@ ExprResult Sema::BuildCXXDefaultInitExpr(SourceLocation Loc, FieldDecl *Field) {
Field->setInvalidDecl();
return ExprError();
}
+ RebuiltInitNeedsCleanups = Immediate.rebuiltInitNeedsCleanups();
Init = Res.get();
}
@@ -5923,8 +5973,11 @@ ExprResult Sema::BuildCXXDefaultInitExpr(SourceLocation Loc, FieldDecl *Field) {
runWithSufficientStackSpace(Loc, [&] {
MarkDeclarationsReferencedInExpr(E, /*SkipLocalVariables=*/false);
});
- if (isInLifetimeExtendingContext())
+ if (isInLifetimeExtendingContext()) {
DiscardCleanupsInEvaluationContext();
+ if (RebuiltInitNeedsCleanups)
+ Cleanup.setExprNeedsCleanups(true);
+ }
// C++11 [class.base.init]p7:
// The initialization of each base and member constitutes a
// full-expression.
diff --git a/clang/lib/Sema/SemaExprCXX.cpp b/clang/lib/Sema/SemaExprCXX.cpp
index 39c5e3b0671bb..0731f3289952c 100644
--- a/clang/lib/Sema/SemaExprCXX.cpp
+++ b/clang/lib/Sema/SemaExprCXX.cpp
@@ -13,6 +13,7 @@
#include "TreeTransform.h"
#include "TypeLocBuilder.h"
+#include "clang/AST/ASTConsumer.h"
#include "clang/AST/ASTContext.h"
#include "clang/AST/ASTLambda.h"
#include "clang/AST/CXXInheritance.h"
@@ -6722,7 +6723,15 @@ ExprResult Sema::MaybeBindToTemporary(Expr *E) {
CXXDestructorDecl *Destructor = IsDecltype ? nullptr : LookupDestructor(RD);
if (Destructor) {
+ bool HadBody = Destructor->doesThisDeclarationHaveABody();
MarkFunctionReferenced(E->getExprLoc(), Destructor);
+ // MarkFunctionReferenced can synthesize an implicit destructor. If that
+ // happens here, there may be no later top-level definition callback for
+ // CodeGen to see, for example for a closure type in a default member
+ // initializer.
+ if (Destructor->isImplicit() && !HadBody &&
+ Destructor->doesThisDeclarationHaveABody())
+ Consumer.HandleTopLevelDecl(DeclGroupRef(Destructor));
CheckDestructorAccess(E->getExprLoc(), Destructor,
PDiag(diag::err_access_dtor_temp)
<< E->getType());
diff --git a/clang/test/CodeGenCXX/gh196469-default-member-init-lambda-cleanup.cpp b/clang/test/CodeGenCXX/gh196469-default-member-init-lambda-cleanup.cpp
new file mode 100644
index 0000000000000..71857dc449416
--- /dev/null
+++ b/clang/test/CodeGenCXX/gh196469-default-member-init-lambda-cleanup.cpp
@@ -0,0 +1,24 @@
+// RUN: %clang_cc1 -std=c++20 -triple x86_64-unknown-linux-gnu -emit-llvm -o - %s | FileCheck %s
+
+struct Noisy {
+ Noisy();
+ ~Noisy();
+};
+
+struct Function {
+ template <typename F> Function(F) {}
+};
+
+struct Options {
+ Function function{[noisy = Noisy{}] {}};
+};
+
+Options kOptions{};
+
+// CHECK-LABEL: define internal void @__cxx_global_var_init
+// CHECK: call void @_ZN5NoisyC1Ev
+// CHECK: call void @_ZN8FunctionC1IN7Options8functionMUlvE_EEET_
+// CHECK: call void @_ZN7Options8functionMUlvE_D1Ev
+
+// CHECK-LABEL: define {{.*}} @_ZN7Options8functionMUlvE_D2Ev
+// CHECK: call void @_ZN5NoisyD1Ev
More information about the cfe-commits
mailing list